Android BLE: onCharacteristicChanged never fires

mbm29414 picture mbm29414 · Feb 18, 2015 · Viewed 8.7k times · Source

I'm trying to write an Android app that mimics functionality already present in an iOS app I wrote. I am interfacing with 2 different BLE devices:

  1. Blood Pressure Cuff
  2. Weight Scale

On iOS, I have both devices working well and reporting data. On Android, I can't get it to work. After hours of research and testing, I think the basic issue I'm trying to solve is this:

On iOS, I call the following code to enable the BLE device to notify my iOS device when it has data to report:

#pragma mark - CBPeripheralDelegate Protocol methods
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    for (CBCharacteristic *characteristic in [service characteristics]) {
        [peripheral setNotifyValue:YES forCharacteristic:characteristic];
    }
}

That's it. The notes for this method in iOS say the following:

If the specified characteristic is configured to allow both notifications and indications, calling this method enables notifications only.

Based on that (and the fact that it works in iOS), I'm figuring that the configuration descriptor for the characteristic for which I want notifications should be configured like this:

descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
gatt.writeDescriptor(descriptor);

With that in mind, my BLEDevice class looks like this:

public abstract class BLEDevice {
    protected BluetoothAdapter.LeScanCallback   mLeScanCallback;
    protected BluetoothGattCallback             mBluetoothGattCallback;
    protected byte[]                            mBytes;
    protected Context                           mContext;
    protected GotReadingCallback                mGotReadingCallback;
    protected String                            mDeviceName;
    public final static UUID                    UUID_WEIGHT_SCALE_SERVICE                       
            = UUID.fromString(GattAttributes.WEIGHT_SCALE_SERVICE);
    public final static UUID                    UUID_WEIGHT_SCALE_READING_CHARACTERISTIC        
            = UUID.fromString(GattAttributes.WEIGHT_SCALE_READING_CHARACTERISTIC);
    public final static UUID                    UUID_WEIGHT_SCALE_CONFIGURATION_CHARACTERISTIC  
            = UUID.fromString(GattAttributes.WEIGHT_SCALE_CONFIGURATION_CHARACTERISTIC);
    public final static UUID                    UUID_WEIGHT_SCALE_CONFIGURATION_DESCRIPTOR      
            = UUID.fromString(GattAttributes.WEIGHT_SCALE_CONFIGURATION_DESCRIPTOR);

    abstract void processReading();

    interface GotReadingCallback {
        void gotReading(Object reading);
    }

    public BLEDevice(Context context, String deviceName, GotReadingCallback gotReadingCallback) {
        mContext                    = context;
        BluetoothManager btManager  = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
        final BluetoothAdapter btAdapter  = btManager.getAdapter();
        if (btAdapter != null && !btAdapter.isEnabled()) {
            Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            mContext.startActivity(enableIntent);
        }
        mDeviceName = deviceName;
        mBluetoothGattCallback = new BluetoothGattCallback() {
            @Override
            public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
                byte[] data = characteristic.getValue();
                mBytes = data;
                Log.d("BluetoothGattCallback.onCharacteristicChanged", "data: " + data.toString());
            }
            @Override
            public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
                // this will get called when a device connects or disconnects
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    gatt.discoverServices();
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    if (mBytes != null) {
                        processReading();
                    }
                }
            }
            @Override
            public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
                super.onDescriptorWrite(gatt, descriptor, status);
                Log.d("onDescriptorWrite", "descriptor: " + descriptor.getUuid() + ". characteristic: " + descriptor.getCharacteristic().getUuid() + ". status: " + status);
            }
            @Override
            public void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
                // this will get called after the client initiates a BluetoothGatt.discoverServices() call
                BluetoothGattService service = gatt.getService(UUID_WEIGHT_SCALE_SERVICE);
                if (service != null) {
                    BluetoothGattCharacteristic characteristic;
                    characteristic                              = service.getCharacteristic(UUID_WEIGHT_SCALE_READING_CHARACTERISTIC);
                    if (characteristic != null) {
                        gatt.setCharacteristicNotification(characteristic, true);
                    }
                    characteristic                              = service.getCharacteristic(UUID_WEIGHT_SCALE_CONFIGURATION_CHARACTERISTIC);
                    if (characteristic != null) {
                        BluetoothGattDescriptor descriptor      = characteristic.getDescriptor(UUID_WEIGHT_SCALE_CONFIGURATION_DESCRIPTOR);
                        if (descriptor != null) {
                            descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
                            gatt.writeDescriptor(descriptor);
                        }
                    }
                }
            }
        };
        mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
            @Override
            public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
                Log.d("LeScanCallback", device.toString());
                if (device.getName().contains("{Device Name}")) {
                    BluetoothGatt bluetoothGatt = device.connectGatt(mContext, false, mBluetoothGattCallback);
                    btAdapter.stopLeScan(mLeScanCallback);
                }
            }
        };
        btAdapter.startLeScan(mLeScanCallback);
    }
}

NOTE: It might be important to know that these 2 devices function in the following way:

  1. The BLE device is turned on an a measurement is initiated on the device.
  2. Once the measurement has been taken, the BLE device attempts to initiate a BLE connection.
  3. Once the BLE connection is made, the device pretty much immediately sends the data, sometimes sending a couple of data packets. (If previous data measurements haven't been successfully sent over BLE, it keeps them in memory and sends all of them, so I only really care about the final data packet.)
  4. Once the final data packet is sent, the BLE device disconnects rapidly.
  5. If the BLE device fails to send data (as is currently happening on the Android app), the BLE device disconnects pretty rapidly.

In my LogCat, I see a lot of output that's exactly like I'd expect.

  1. I see a list of services like I expect, including the data service I want.
  2. I see a list of characteristics like I expect, including the data characteristic I want.
  3. I see a list of descriptors like I expect, including the "configuration" (0x2902) descriptor.

The most recent failure I'm experiencing is a status of "128" being reported in onCharacteristicWrite. The comments to question #3 (below) seem to indicate this is a resource issue.

I've looked at the following questions:

  1. Android BLE onCharacteristicChanged not called
  2. Android BLE, read and write characteristics
  3. Android 4.3 onDescriptorWrite returns status 128

Here's why they don't give me what I need:

  1. This question's answer was not to read the descriptor's value. I'm not doing that, so that can't be what's getting in the way.
  2. This is basically an overview of the various methods that are available, which I think I now understand. The big key in this question/answer is not to write multiple times to different descriptors, but I'm also not doing that. I only care about the one characteristic.
  3. This question/answer seems to be related to BLE resource limitations, but I don't think this applies. I'm only connecting this one device and I'm trying to do a very, very simple data transfer. I don't think I'm hitting resource ceilings.

I've tried a bunch of examples and tutorials, including Google's Android sample code. None of them seem to enable the BLE device to notify my Android device of data updates. It's obviously not the device, since the iOS version works. So, what is the iOS code doing in the background to get the notifications to work and what code on the Android side will mimic that functionality?


EDIT/UPDATE

Based on @yonran's comments, I updated my code by changing the onServicesDiscovered implementation to this:

@Override
public void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
    // this will get called after the client initiates a BluetoothGatt.discoverServices() call
    BluetoothGattService service = gatt.getService(UUID_WEIGHT_SCALE_SERVICE);
    if (service != null) {
        BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID_WEIGHT_SCALE_READING_CHARACTERISTIC);
        if (characteristic != null) {
            if (gatt.setCharacteristicNotification(characteristic, true) == true) {
                Log.d("gatt.setCharacteristicNotification", "SUCCESS!");
            } else {
                Log.d("gatt.setCharacteristicNotification", "FAILURE!");
            }
            BluetoothGattDescriptor descriptor = characteristic.getDescriptors().get(0);
            if (0 != (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE)) {
                // It's an indicate characteristic
                Log.d("onServicesDiscovered", "Characteristic (" + characteristic.getUuid() + ") is INDICATE");
                if (descriptor != null) {
                    descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
                    gatt.writeDescriptor(descriptor);
                }
            } else {
                // It's a notify characteristic
                Log.d("onServicesDiscovered", "Characteristic (" + characteristic.getUuid() + ") is NOTIFY");
                if (descriptor != null) {
                    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                    gatt.writeDescriptor(descriptor);
                }
            }
        }
    }
}

That does seem to have changed some things a little bit. Here's the current Logcat, following that code change:

D/BluetoothGatt﹕ setCharacteristicNotification() - uuid: <UUID> enable: true
D/gatt.setCharacteristicNotification﹕ SUCCESS!
D/onServicesDiscovered﹕ Characteristic (<UUID>) is INDICATE
D/BluetoothGatt﹕ writeDescriptor() - uuid: 00002902-0000-1000-8000-00805f9b34fb
D/BluetoothGatt﹕ onDescriptorWrite() - Device=D0:5F:B8:01:6C:9E UUID=<UUID>
D/onDescriptorWrite﹕ descriptor: 00002902-0000-1000-8000-00805f9b34fb. characteristic: <UUID>. status: 0
D/BluetoothGatt﹕ onClientConnectionState() - status=0 clientIf=6 device=D0:5F:B8:01:6C:9E

So, it would appear that I'm now setting everything up properly (since setCharacteristicNotification returns true and the onDescriptorWrite status is 0). However, onCharacteristicChanged still never fires.


Answer

madcow37 picture madcow37 · Apr 17, 2015

I've been able to successfully catch onCharacteristicChanged() with multiple services and characteristics by:

  1. Writing descriptor values in the broadcastReceiver() in the main loop after service discovery is finished.

    private final BroadcastReceiver UARTStatusChangeReceiver = new BroadcastReceiver() {
    //more code...
    if (action.equals(uartservice.ACTION_GATT_SERVICES_DISCOVERED)) {
             mService.enableTXNotification();
        }
    

and

  1. By adding a delay between descriptor value settings

    public void enableTXNotification(){ 
    /*
    if (mBluetoothGatt == null) {
        showMessage("mBluetoothGatt null" + mBluetoothGatt);
        broadcastUpdate(DEVICE_DOES_NOT_SUPPORT_UART);
        return;
    }
        */
    /**
     * Enable Notifications for the IO service and characteristic
     *
     */
    BluetoothGattService IOService = mBluetoothGatt.getService(IO_SERVICE_UUID);
    if (IOService == null) {
        showMessage("IO service not found!");
        broadcastUpdate(DEVICE_DOES_NOT_SUPPORT_IO);
        return;
    }
    BluetoothGattCharacteristic IOChar = IOService.getCharacteristic(IO_CHAR_UUID);
    if (IOChar == null) {
        showMessage("IO charateristic not found!");
        broadcastUpdate(DEVICE_DOES_NOT_SUPPORT_IO);
        return;
    }
    mBluetoothGatt.setCharacteristicNotification(IOChar,true);
    BluetoothGattDescriptor descriptorIO = IOChar.getDescriptor(CCCD);
    descriptorIO.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    mBluetoothGatt.writeDescriptor(descriptorIO);
    
    /**
     * For some reason android (or the device) can't handle
     * writing one descriptor after another properly.  Without
     * the delay only the first characteristic can be caught in
     * onCharacteristicChanged() method.
     */
    try {
        Thread.sleep(200);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    /**
     * Enable Indications for the RXTX service and characteristic
     */
    BluetoothGattService RxService = mBluetoothGatt.getService(RXTX_SERVICE_UUID);
    if (RxService == null) {
        showMessage("Rx service not found!");
        broadcastUpdate(DEVICE_DOES_NOT_SUPPORT_UART);
        return;
    }
    BluetoothGattCharacteristic RxChar = RxService.getCharacteristic(RXTX_CHAR_UUID);
    if (RxChar == null) {
        showMessage("Tx charateristic not found!");
        broadcastUpdate(DEVICE_DOES_NOT_SUPPORT_UART);
        return;
    }
    mBluetoothGatt.setCharacteristicNotification(RxChar,true);
    BluetoothGattDescriptor descriptor = RxChar.getDescriptor(CCCD);
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE );
    mBluetoothGatt.writeDescriptor(descriptor);
    
    try {
        Thread.sleep(200);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    /**
     * Enable Notifications for the Battery service and Characteristic?
     */
    BluetoothGattService batteryService = mBluetoothGatt.getService(BATTERY_SERVICE_UUID);
    if (batteryService == null) {
        showMessage("Battery service not found!");
        broadcastUpdate(DEVICE_DOES_NOT_SUPPORT_BATTERY);
        return;
    }
    BluetoothGattCharacteristic batteryChar = batteryService.getCharacteristic(BATTERY_CHAR_UUID);
    if (batteryChar == null) {
        showMessage("Battery charateristic not found!");
        broadcastUpdate(DEVICE_DOES_NOT_SUPPORT_BATTERY);
        return;
    }
    }