75
votes

Device used for testing: Nexus 4, Android 4.3

Connection is working fine but the onCharacteristicChangedMethod of my callback is never called. However I am registering for notifications using setCharacteristicNotification(char, true) inside onServicesDiscovered and that function even returns true.

Device log (there are actually no messages at all when notifications should appear / are sent via the Bluetooth device):

07-28 18:15:06.936  16777-16809/de.ffuf.leica.sketch D/BluetoothGatt: setCharacteristicNotification() - uuid: 3ab10101-f831-4395-b29d-570977d5bf94 enable: true
07-28 18:15:06.936    4372-7645/com.android.bluetooth D/BtGatt.GattService: registerForNotification() - address=C9:79:25:34:19:6C enable: true
07-28 18:15:06.936    4372-7645/com.android.bluetooth D/BtGatt.btif: btif_gattc_reg_for_notification
07-28 18:15:06.946    4372-7645/com.android.bluetooth D/BtGatt.btif: btgattc_handle_event: Event 1018
07-28 18:15:06.946    4372-7645/com.android.bluetooth D/BtGatt.GattService: onRegisterForNotifications() - address=null, status=0, registered=1, charUuid=3ab10101-f831-4395-b29d-570977d5bf94
07-28 18:15:06.946    4372-7645/com.android.bluetooth D/BtGatt.btif: btgattc_handle_event: Event 1016
07-28 18:15:06.946    4372-7645/com.android.bluetooth D/BtGatt.btif: btgattc_handle_event: Event 1018
07-28 18:15:06.946    4372-7645/com.android.bluetooth D/BtGatt.GattService: onRegisterForNotifications() - address=null, status=0, registered=1, charUuid=3ab10102-f831-4395-b29d-570977d5bf94
07-28 18:15:06.946    4372-7645/com.android.bluetooth D/BtGatt.btif: btgattc_handle_event: Event 1016
07-28 18:15:06.946    4372-7684/com.android.bluetooth E/bt-btif: already has a pending command!!
07-28 18:15:06.946    4372-7645/com.android.bluetooth D/BtGatt.btif: btgattc_handle_event: Event 1013
07-28 18:15:06.946    4372-7684/com.android.bluetooth E/bt-btif: already has a pending command!!
07-28 18:15:06.946    4372-7645/com.android.bluetooth D/BtGatt.btif: btgattc_handle_event: Event 1013
07-28 18:15:06.946    4372-7684/com.android.bluetooth E/bt-btif: already has a pending command!!
07-28 18:15:06.976    4372-7645/com.android.bluetooth D/BtGatt.btif: btif_gattc_upstreams_evt: Event 9

GATT Notifications work fine using iOS and the app basically does the same as on Android (registering for notification etc.).

Has anyone else experienced this with a possible solution?

10

10 Answers

84
votes

It seems like you forgot to write the Descriptor which tells your BLE device to go in this mode. See the code lines that deal with descriptor at http://developer.android.com/guide/topics/connectivity/bluetooth-le.html#notification

Without setting this descriptor, you never receive updates to a characteristic. Calling setCharacteristicNotification is not enough. This is a common mistake.

code snipped

protected static final UUID CHARACTERISTIC_UPDATE_NOTIFICATION_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

public boolean setCharacteristicNotification(BluetoothDevice device, UUID serviceUuid, UUID characteristicUuid,
        boolean enable) {
    if (IS_DEBUG)
        Log.d(TAG, "setCharacteristicNotification(device=" + device.getName() + device.getAddress() + ", UUID="
                + characteristicUuid + ", enable=" + enable + " )");
    BluetoothGatt gatt = mGattInstances.get(device.getAddress()); //I just hold the gatt instances I got from connect in this HashMap
    BluetoothGattCharacteristic characteristic = gatt.getService(serviceUuid).getCharacteristic(characteristicUuid);
    gatt.setCharacteristicNotification(characteristic, enable);
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CHARACTERISTIC_UPDATE_NOTIFICATION_DESCRIPTOR_UUID);
    descriptor.setValue(enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : new byte[] { 0x00, 0x00 });
    return gatt.writeDescriptor(descriptor); //descriptor write operation successfully started? 
}
42
votes

@Boni2k - I have the same issues. In my case, I have 3 notifying characteristics and a handful of read/write characteristics.

What I did find is that there is some dependency between writeGattDescriptor and readCharacteristic. All of the writeGattDescriptors must come first and complete before you issue any readCharacteristic calls.

Here is my solution using Queues. Now I am getting notifications and everything else works fine:

Create two Queues like this:

private Queue<BluetoothGattDescriptor> descriptorWriteQueue = new LinkedList<BluetoothGattDescriptor>();
private Queue<BluetoothGattCharacteristic> characteristicReadQueue = new LinkedList<BluetoothGattCharacteristic>();

Then write all of your descriptors immediately after discovery with this method:

public void writeGattDescriptor(BluetoothGattDescriptor d){
    //put the descriptor into the write queue
    descriptorWriteQueue.add(d);
    //if there is only 1 item in the queue, then write it.  If more than 1, we handle asynchronously in the callback above
    if(descriptorWriteQueue.size() == 1){   
        mBluetoothGatt.writeDescriptor(d);      
    }
}

and this callback:

public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {         
        if (status == BluetoothGatt.GATT_SUCCESS) {
            Log.d(TAG, "Callback: Wrote GATT Descriptor successfully.");           
        }           
        else{
            Log.d(TAG, "Callback: Error writing GATT Descriptor: "+ status);
        }
        descriptorWriteQueue.remove();  //pop the item that we just finishing writing
        //if there is more to write, do it!
        if(descriptorWriteQueue.size() > 0)
            mBluetoothGatt.writeDescriptor(descriptorWriteQueue.element());
        else if(readCharacteristicQueue.size() > 0)
            mBluetoothGatt.readCharacteristic(readQueue.element());
    };

The method for reading a characteristic normally then looks like this:

public void readCharacteristic(String characteristicName) {
    if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothAdapter not initialized");
        return;
    }
    BluetoothGattService s = mBluetoothGatt.getService(UUID.fromString(kYourServiceUUIDString));
    BluetoothGattCharacteristic c = s.getCharacteristic(UUID.fromString(characteristicName));
    //put the characteristic into the read queue        
    readCharacteristicQueue.add(c);
    //if there is only 1 item in the queue, then read it.  If more than 1, we handle asynchronously in the callback above
    //GIVE PRECEDENCE to descriptor writes.  They must all finish first.
    if((readCharacteristicQueue.size() == 1) && (descriptorWriteQueue.size() == 0))
        mBluetoothGatt.readCharacteristic(c);              
}

and my read callback:

public void onCharacteristicRead(BluetoothGatt gatt,
                                     BluetoothGattCharacteristic characteristic,
                                     int status) {
        readCharacteristicQueue.remove();
        if (status == BluetoothGatt.GATT_SUCCESS) {
            broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);                                
        }
        else{
            Log.d(TAG, "onCharacteristicRead error: " + status);
        }

        if(readCharacteristicQueue.size() > 0)
            mBluetoothGatt.readCharacteristic(readCharacteristicQueue.element());
    }
11
votes

When setting the value to the descriptor instead of putting descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE), put descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE). The callbacks for onCharacteristicChanged are called now.

6
votes

I assume (you did not provide your source code) that you did not implement it as Google wanted:

(1)

mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);

and then

(2)

BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);

I suppose 2 is missing. In that case I believe on low-level notification will be triggered but they will never be reported to application layer.

6
votes

Experienced issues in earlier versions of Android receiving notifications (an indication that was registered) and always had a strange disconnect event afterwards. As it turns out, this was because we registered for notifications on five characteristics.

The error discovered in LogCat was:

02-05 16:14:24.990    1271-1601/? E/bt-btif﹕ Max Notification Reached, registration failed.

Prior to 4.4.2, the number of registrations was capped at 4! 4.4.2 increased this limit to 7.

By reducing the number of registrations in earlier versions, we were able to step around this limitation.

4
votes

Well, this API name surely lead some confusions to app developer if he/she was not the Bluetooth background programmer.

From Bluetooth core specification perspective, quote from core spec 4.2 Vol 3, Part G section 3.3.3.3 "Client Characteristic Configuration" :

The characteristic descriptor value is a bit field. When a bit is set, that action shall be enabled, otherwise it will not be used.

and section 4.10

Notifications can be configured using the Client Characteristic Configuration descriptor (See Section 3.3.3.3).

which is clearly states that if client want to receive the notification(or indication,which need response) from server, should write the "Notification" bit to 1("Indication" bit also to 1 otherwise).

However, the name "setCharacteristicNotification" give us a hint is that if we set the parameters of this API as TURE, the client would got notifications; unfortunately this API only set the local bit to allow the notification sent to apps in case of remote notification comes. See code from Bluedroid:

    /*******************************************************************************
    **
    ** Function         BTA_GATTC_RegisterForNotifications
    **
    ** Description      This function is called to register for notification of a service.
    **
    ** Parameters       client_if - client interface.
    **                  bda - target GATT server.
    **                  p_char_id - pointer to GATT characteristic ID.
    **
    ** Returns          OK if registration succeed, otherwise failed.
    **
    *******************************************************************************/

    tBTA_GATT_STATUS BTA_GATTC_RegisterForNotifications (tBTA_GATTC_IF client_if,
                                                         BD_ADDR bda,
                                                         tBTA_GATTC_CHAR_ID *p_char_id)

{
    tBTA_GATTC_RCB      *p_clreg;
    tBTA_GATT_STATUS    status = BTA_GATT_ILLEGAL_PARAMETER;
    UINT8               i;

    if (!p_char_id)
    {
        APPL_TRACE_ERROR("deregistration failed, unknow char id");
        return status;
    }

    if ((p_clreg = bta_gattc_cl_get_regcb(client_if)) != NULL)
    {
        for (i = 0; i < BTA_GATTC_NOTIF_REG_MAX; i ++)
        {
            if ( p_clreg->notif_reg[i].in_use &&
                 !memcmp(p_clreg->notif_reg[i].remote_bda, bda, BD_ADDR_LEN) &&
                  bta_gattc_charid_compare(&p_clreg->notif_reg[i].char_id, p_char_id))
            {
                APPL_TRACE_WARNING("notification already registered");
                status = BTA_GATT_OK;
                break;
            }
        }
        if (status != BTA_GATT_OK)
        {
            for (i = 0; i < BTA_GATTC_NOTIF_REG_MAX; i ++)
            {
                if (!p_clreg->notif_reg[i].in_use)
                {
                    memset((void *)&p_clreg->notif_reg[i], 0, sizeof(tBTA_GATTC_NOTIF_REG));

                    p_clreg->notif_reg[i].in_use = TRUE;
                    memcpy(p_clreg->notif_reg[i].remote_bda, bda, BD_ADDR_LEN);

                    p_clreg->notif_reg[i].char_id.srvc_id.is_primary = p_char_id->srvc_id.is_primary;
                    bta_gattc_cpygattid(&p_clreg->notif_reg[i].char_id.srvc_id.id, &p_char_id->srvc_id.id);
                    bta_gattc_cpygattid(&p_clreg->notif_reg[i].char_id.char_id, &p_char_id->char_id);

                    status = BTA_GATT_OK;
                    break;
                }
            }
            if (i == BTA_GATTC_NOTIF_REG_MAX)
            {
                status = BTA_GATT_NO_RESOURCES;
                APPL_TRACE_ERROR("Max Notification Reached, registration failed.");
            }
        }
    }
    else
    {
        APPL_TRACE_ERROR("Client_if: %d Not Registered", client_if);
    }

    return status;
}'

so what matters was the descriptor write action.

1
votes

Here's a simple way to do it, but let me know if you see any drawbacks.

Step 1 Declare boolean variables

private boolean char_1_subscribed = false;
private boolean char_2_subscribed = false;
private boolean char_3_subscribed = false;

Step 2 subscribe to the first characteristic in the onServicesDiscovered callback:

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
    } else {
        Log.w(TAG, "onServicesDiscovered received: " + status);
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    if(!char_1_subscribed)
        subscribeToNotification(gatt.getService(UUID_SERVICE).getCharacteristic(UUID_CHAR_1)); char_1_subscribed = true;
}

Step 3

Subscribe to any others after the onCharacteristicChanged callback fires

@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
                                    BluetoothGattCharacteristic characteristic) {
    if(UUID_CHAR_1.equals(characteristic.getUuid()))
    {
        if(!char_1_subscribed)
            subscribeToNotification(gatt.getService(UUID_SERVICE).getCharacteristic(UUID_CHAR_2)); char_2_subscribed = true;
    }
    if(UUID_CHAR_2.equals(characteristic.getUuid()))
    {
        if(!char_3_subscribed)
            subscribeToNotification(gatt.getService(UUID_SERVICE).getCharacteristic(UUID_CHAR_3)); char_3_subscribed = true;
    }
}
1
votes

This one is working for me:

to notify master device that some characteristic is change, call this function on your pheripheral:

private BluetoothGattServer server;
//init....

//on BluetoothGattServerCallback...

//call this after change the characteristic
server.notifyCharacteristicChanged(device, characteristic, false);

in your master device: enable setCharacteristicNotification after discover the service:

@Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        services = mGatt.getServices();
        for(BluetoothGattService service : services){
            if( service.getUuid().equals(SERVICE_UUID)) {
                characteristicData = service.getCharacteristic(CHAR_UUID);
                for (BluetoothGattDescriptor descriptor : characteristicData.getDescriptors()) {
                    descriptor.setValue( BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
                    mGatt.writeDescriptor(descriptor);
                }
                gatt.setCharacteristicNotification(characteristicData, true);
            }
        }
        if (dialog.isShowing()){
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    dialog.hide();
                }
            });
        }
   }

now you can check your characteristic value is change, for example onCharacteristicRead function (this also working on onCharacteristicChanged function as well) :

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        Log.i("onCharacteristicRead", characteristic.toString());
        byte[] value=characteristic.getValue();
        String v = new String(value);
        Log.i("onCharacteristicRead", "Value: " + v);
}
0
votes

I had another reason that I would like to add as it drove me crazy the whole day:

On my Samsung Note 3 I did not receive notifications of changed values while the same code worked on any other device I tested with.

Rebooting the device solved all the problems. Obvious, but when you are in the problem, you forget to think of.

-1
votes

I've experienced the problems with notifications for BLE on Android as well. However there's a fully working demo that includes a bluetooth wrapper around BluetoothAdapter. The wrapper is called BleWrapper and ships with the demo application called BLEDemo contained in the Application Accelerator package. Download here: https://developer.bluetooth.org/Pages/Bluetooth-Android-Developers.aspx. You need to register with your email address at the top right before downloading. The project's license allows for free use, code modification and publication.

To my experience the Android demo application handles BLE notification subscriptions very well. I've not yet dived too much into the code to see how the wrapper actually wraps.

There's an Android app available in Play Store that is a customization of the Application accelerator demo. As the user interface looks nearly the same I suppose that it also uses BleWrapper. Download the app here: https://play.google.com/store/apps/details?id=com.macdom.ble.blescanner