ordsen
ordsen

Reputation: 276

BLE GATT onCharacteristicChanged not called after subscribing to notification

this is my first post on SO.

I have some problems subscribing to GATT notifications on android 5.0.2 .

What I aim to do is to connect an Arduino with a BLE Shield to my Android phone. I have a sensor connected to the Arduino and want to send the data from the Arduino to my phone by using the BLE shield. There is a nRF8001 on the shield which is the server, my phone/app is the client.

What I did so far was to create an Android app which scans for BLE devices. It can connect to a device and read or write characteristics. So, I can "manually" read the characteristic by calling gatt.readCharacteristic(mCharacteristic);. This allows me to get the sensor values from the Arduino. I also created a custom Service by using nRFGo Studio. I know that this part is working as I am able to discover, connect and even to be notified about changes of the characteristic by using the BLE Scanner app which is available on Google Play. But subscribing to the notifications in my own app won't work. Well, at least the subscription works, but onCharacteristicChanged(...) is never called. What's funny is the fact that if I subscribe to the characteristic in my app and afterwards subscribe to it with the BLE scanner app suddenly onCharacteristicChanged(...) is called until I unsubscribe again through the BLE scanner app. (I can see this by the Log)

My android code is as follows: The GATT callback:

private final BluetoothGattCallback mGattCallback =
        new BluetoothGattCallback() {
            @Override
            public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                                int newState) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    sendBroadcastConnected();

                    Log.i("BLE", "Connected to GATT server.");
                    Log.i("BLE", "Attempting to start service discovery:" + bleManager.startServiceDiscovery());

                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    sendBroadcastDisconnected();
                    Log.i("BLE", "Disconnected from GATT server.");
                }
            }

            @Override
            public void onServicesDiscovered(BluetoothGatt gatt, int status) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    Log.w("BLE", "onServicesDiscovered ");
                    setNotifySensor(gatt);     
                }
            }

            private void setNotifySensor(BluetoothGatt gatt) {
                BluetoothGattCharacteristic characteristic = gatt.getService(Globals.MPU_SERVICE_UUID).getCharacteristic(Globals.X_ACCEL_CHARACTERISTICS_UUID);
                gatt.setCharacteristicNotification(characteristic, true);

                BluetoothGattDescriptor desc = characteristic.getDescriptor(Globals.X_ACCEL_DESCRIPTOR_UUID);
                Log.i("BLE", "Descriptor is " + desc); // this is not null
                desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                Log.i("BLE", "Descriptor write: " + gatt.writeDescriptor(desc)); // returns true

            }

            @Override
            public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
                               if(Globals.X_ACCEL_CHARACTERISTICS_UUID.equals(characteristic.getUuid())){
                    Log.w("BLE", "CharacteristicRead - xaccel service uuid: " + characteristic.getService().getUuid());
                    Log.w("BLE", "CharacteristicRead - xaccel value: " + characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8,0));
                }
            }

            @Override
            public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
                Log.i("BLE", "Received characteristics changed event : "+characteristic.getUuid());
                if(Globals.X_ACCEL_CHARACTERISTICS_UUID.equals(characteristic.getUuid())){
                    Log.i("BLE", "Received new value for xAccel.");
                }


            }

            @Override
            public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            }

            @Override
            public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            }

            @Override
            public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            }
        };

This is how I connect to GATT:

        bt_device = createBluetoothDevice(DEVICE_ADRESS);
        BluetoothGatt mBluetoothGatt = bt_device.connectGatt(context, false, mGattCallback);

All of this code runs in a background service which is started after a BLE device is selected.

What I tried was to implement things like they are demonstrated in the Google Dev API Guide and I also tried this solution (without success). Neither searching on Google, reading questions already asked on SO (For example this one ) or having a look on the Nordic Developer Zone did help much in this case.

Finally, my question is: What have I done wrong? Am I missing something? I just can't figure it out and it's driving me crazy for two days now. I don't know where else I could search for a solution, so I hope you can help me..

EDIT Globals Class:

// BLE Services
public static final UUID MPU_SERVICE_UUID = UUID.fromString("3f540001-1ee0-4245-a7ef-35885ccae141");
// BLE Characteristics
public static final UUID X_ACCEL_CHARACTERISTICS_UUID = UUID.fromString("3f540002-1ee0-4245-a7ef-35885ccae141");
// BLE Descriptors
public static final UUID X_ACCEL_DESCRIPTOR_UUID = UUID.fromString("3f542902-1ee0-4245-a7ef-35885ccae141");

EDIT What i do in BLE Scanner: I simply scan for BLE devices. It finds my device and after tapping on it it shows me all my services which I set up on the Arduino board. After selecting a service it shows me all my chatacteristics which I specified for this Service. And when I tap on a characteristic, it shows me its UUID and its Service's UUID. Also, the BLE Scanner app allows me to subscribe to the notification or to read the characteristic. When I subscribe, the value is continously updated.

Upvotes: 14

Views: 19918

Answers (3)

BaiJiFeiLong
BaiJiFeiLong

Reputation: 4665

In Android SDK 33, onCharacteristicChanged is deprecated but the alternative not worked!!!

You should use onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic), not onCharacteristicChanged(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) !!!

/** @deprecated */
@Deprecated
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    throw new RuntimeException("Stub!");
}

public void onCharacteristicChanged(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
    throw new RuntimeException("Stub!");
}

One completed worked example:

package io.github.baijifeilong.bluetooth;

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import com.google.common.collect.Iterables;

import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;


public class BluetoothActivity extends AppCompatActivity implements View.OnClickListener, AdapterView.OnItemClickListener {
    private ArrayAdapter<String> deviceListAdapter;
    private final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    private final Logger logger = LoggerFactory.getLogger(BluetoothActivity.class);
    private BluetoothGatt bluetoothGatt;
    private ArrayAdapter<String> messageListAdapter;
    private Button findButton;

    @SuppressLint("SetTextI18n")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        LinearLayout rootLayout = new LinearLayout(this);
        rootLayout.setOrientation(LinearLayout.VERTICAL);

        TextView deviceListTitleTextView = new TextView(this);
        deviceListTitleTextView.setText("Device List");
        deviceListTitleTextView.setBackgroundResource(android.R.color.holo_blue_light);
        ListView deviceListView = new ListView(this);
        LinearLayout.LayoutParams deviceListParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
        deviceListParams.weight = 1;
        deviceListView.setLayoutParams(deviceListParams);

        TextView messageListTitleTextView = new TextView(this);
        messageListTitleTextView.setBackgroundResource(android.R.color.holo_blue_light);
        messageListTitleTextView.setText("Message List");
        ListView messageListView = new ListView(this);
        LinearLayout.LayoutParams messageListParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
        messageListParams.weight = 1;
        messageListView.setLayoutParams(messageListParams);

        findButton = new Button(this);
        findButton.setText("Find");
        Button sendButton = new Button(this);
        sendButton.setText("Send");
        rootLayout.addView(deviceListTitleTextView);
        rootLayout.addView(deviceListView);
        rootLayout.addView(messageListTitleTextView);
        rootLayout.addView(messageListView);
        rootLayout.addView(findButton);
        rootLayout.addView(sendButton);
        setContentView(rootLayout);

        findButton.setOnClickListener(this);
        sendButton.setOnClickListener(this);
        deviceListAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
        deviceListView.setAdapter(deviceListAdapter);
        deviceListView.setOnItemClickListener(this);
        messageListAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
        messageListView.setAdapter(messageListAdapter);
        this.registerReceiver(bluetoothReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
        this.registerReceiver(bluetoothReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_STARTED));
        this.registerReceiver(bluetoothReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));

        LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        locationManager.getLastKnownLocation(locationManager.getAllProviders().get(0)); // For XiaoMi users !!!
        logger.info("Logger worked: {}", true);
    }

    @Override
    public void onClick(View v) {
        if (v == findButton) {
            logger.info("Scanning...");
            Toast.makeText(this, "Scanning...", Toast.LENGTH_SHORT).show();
            if (bluetoothGatt != null) {
                bluetoothGatt.disconnect();
            }
            deviceListAdapter.clear();
            bluetoothAdapter.startDiscovery();
        } else {
            bluetoothAdapter.cancelDiscovery();
            if (writerCharacteristic == null) {
                Toast.makeText(this, "Not Ready", Toast.LENGTH_SHORT).show();
                return;
            }
            String message = "Hello World\n";
            logger.info("Sending message {}", StringEscapeUtils.escapeJava(message));
            messageListAdapter.add(StringEscapeUtils.escapeJava(message));
            writerCharacteristic.setValue(message.getBytes());
            boolean writeResult = bluetoothGatt.writeCharacteristic(writerCharacteristic);
            logger.info("Write result: {}", writeResult);
            assert writeResult;
        }
    }

    private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            logger.info("Broadcast received: {}", intent.getAction());
            if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                deviceListAdapter.add(String.format("%s\n%s", device.getName(), device.getAddress()));
                deviceListAdapter.notifyDataSetChanged();
            }
        }
    };

    private BluetoothGattCharacteristic writerCharacteristic;
    private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            logger.info("Connection state changed: {} => {}", status, newState);
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                gatt.requestMtu(200);
            }
        }

        @Override
        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
            super.onMtuChanged(gatt, mtu, status);
            logger.info("MTU Changed: {}", mtu);
            gatt.discoverServices();
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            logger.info("Service discovered: {}", status);
            BluetoothGattService service = Iterables.getLast(gatt.getServices());
            logger.info("Service UUID: {}", service.getUuid());
            writerCharacteristic = null;
            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                if (isCharacteristicNotifiable(characteristic)) {
                    logger.info("Notifier UUID: {}", characteristic.getUuid());
                    boolean setNotificationResult = gatt.setCharacteristicNotification(characteristic, true);
                    logger.info("SetNotificationResult: {}", setNotificationResult);
                    BluetoothGattDescriptor descriptor = characteristic.getDescriptors().get(0);
                    boolean setValueResult = descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                    logger.info("SetValueResult: {}", setValueResult);
                    boolean writeResult = gatt.writeDescriptor(descriptor);
                    logger.info("Write result: {}", writeResult);
                }
                if (isCharacteristicWritable(characteristic)) {
                    logger.info("Writer UUID: {}", characteristic.getUuid());
                    writerCharacteristic = characteristic;
                }
            }
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
            try {
                String text = new String(characteristic.getValue(), "GBK");
                logger.debug("Characteristic changed: {}", text);
                runOnUiThread(() -> messageListAdapter.add(StringEscapeUtils.escapeJava(text)));
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void onCharacteristicChanged(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
            super.onCharacteristicChanged(gatt, characteristic, value);
            logger.info("This method will never not be called!!!");
            assert false;
        }

        @Override
        public void onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value, int status) {
            logger.info("Characteristic read");
            super.onCharacteristicRead(gatt, characteristic, value, status);
        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            logger.info("Characteristic write");
            super.onCharacteristicWrite(gatt, characteristic, status);
        }
    };

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        String address = deviceListAdapter.getItem(position).split("\n")[1];
        deviceListAdapter.clear();
        bluetoothAdapter.cancelDiscovery();
        BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
        logger.info("Current device: {}", device);
        bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);
    }

    public static boolean isCharacteristicWritable(BluetoothGattCharacteristic pChar) {
        return (pChar.getProperties() & (BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) != 0;
    }

    public boolean isCharacteristicNotifiable(BluetoothGattCharacteristic pChar) {
        return (pChar.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0;
    }
}

Upvotes: -1

Seasia Creative Crew
Seasia Creative Crew

Reputation: 321

customBluetoothGatt.setCharacteristicNotification(characteristic, enabled);

    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
    boolean success = customBluetoothGatt.writeDescriptor(descriptor);

Now set the descriptor property ENABLE_INDICATION_VALUE

Upvotes: 3

ordsen
ordsen

Reputation: 276

So, I finnally figured out my mistake :) As you can see above, I'm usinng a UUID with the same base for my descriptor as my characteristics (starting with 3f54XXXX-....)

I changed it to public static final UUID X_ACCEL_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); and now everything is working as expected.

I did not do this before as i thought that there should be a descriptor for each characteristic. But in fact, according to the Client Characteristic Configuration the ...

[...] descriptor shall be persistent across connections for bonded devices. The Client Characteristic Configuration descriptor is unique for each client.

So I checked the RedBearLab Android App example and saw that the descriptor's UUID equals the ones posted on other SO answers.

This also explains why my App received Notifications after I enabled them in the BLE Scanner App: As the descriptor shall be persistent across connections for bonded devices, the BLE Scanner App used also this UUID for the descriptor and thus enabled notifications for the client (= my phone).

Upvotes: 10

Related Questions