13

I have a bluetooth headset which is paired with my Nexus 5X (running Android 7.1) and I would like to connect to a GATT Server of the headset. I tried it with the following code:

private BluetoothGattCallback btleGattCallback = new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        Log.d(TAG, "onConnectionStateChange: " + status + ", " + newState);

        if(newState == STATE_CONNECTED) {
            Log.d(TAG, "Device connected");
            boolean ans = gatt.discoverServices();
            Log.d(TAG, "Discover Services started: " + ans);
        }
    }

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            Log.d(TAG, "Number of Services: " + gatt.getServices().size());
    }
};

public void onDeviceClicked(BluetoothDevice device) {
    BluetoothGatt gatt = device.connectGatt(this, false, btleGattCallback);
    Log.d(TAG, "Connected to GATT: " + gatt.connect());
}

If I click on the headset in my UI onDeviceClicked is called and it comes to this Log output:

<!-- language: lang-none -->
Connected to GATT: true
onConnectionStateChange: 0, 2    // GATT_SUCCESS, STATE_CONNECTED
Device connected
Discover Services started: true

As you can see onServicesDiscovered is never fired. I tried to call connectGatt with TRANSPORT_LE (ref) but then I get a onConnectionStateChange: 133, 0. I also found this question which is why I added the gatt.connect() method as mentioned in answer two.

Do you have any ideas why I don't get the onServicesDiscovered callback?

Community
  • 1
  • 1
Cilenco
  • 6,951
  • 17
  • 72
  • 152
  • In your code example you reference `mBluetoothGatt.discoverServices();` and a local variable `BluetoothGatt gatt = device.connectGatt(this, false, btleGattCallback);`? Is it possible they are not the same instance or device? – ayvazj Jan 07 '17 at 07:56
  • Oh sorry that was for testing reasons. I changed it in the code above but it doesn't work either. – Cilenco Jan 07 '17 at 09:57

6 Answers6

10

BLE on Android can be a little finicky.

Make sure you are calling mBluetoothGatt.discoverServices() on the UI thread.

if(newState == STATE_CONNECTED) {
    Log.d(TAG, "Device connected");
    new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
            boolean ans = mBluetoothGatt.discoverServices();
            Log.d(TAG, "Discover Services started: " + ans);
        }
    });
}

Also try making BluetoothGatt gatt a field variable instead of a local variable.

If you are doing any significant work, try using a library that masks all of the idiosyncrasies so you can focus on the high level logic. https://github.com/Polidea/RxAndroidBle.

Here is an example of how to read a characteristic.

        connectionObservable
                .flatMap(rxBleConnection -> rxBleConnection.readCharacteristic(characteristicUuid))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(bytes -> {
                    readOutputView.setText(new String(bytes));
                    readHexOutputView.setText(HexString.bytesToHex(bytes));
                    writeInput.setText(HexString.bytesToHex(bytes));
                }, this::onReadFailure);

Or with Java 7 syntax

        connectionObservable
                .flatMap(new Func1<RxBleConnection, Observable<byte[]>>() {
                    @Override
                    public Observable<byte[]> call(RxBleConnection rxBleConnection) {
                        return rxBleConnection.readCharacteristic(characteristicUuid);
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<byte[]>() {
                    @Override
                    public void onCompleted() {

                    }

                    @Override
                    public void onError(Throwable e) {
                        onReadFailure(e);
                    }

                    @Override
                    public void onNext(byte[] bytes) {
                        readOutputView.setText(new String(bytes));
                        readHexOutputView.setText(HexString.bytesToHex(bytes));
                        writeInput.setText(HexString.bytesToHex(bytes));
                    }
                });
ayvazj
  • 3,943
  • 1
  • 14
  • 14
  • This does not help either but the API looks good :) Can you give me an example how to read e.g the battery level? I'm a bit confused with the new lambdas and the method referencing in the examples... – Cilenco Jan 07 '17 at 22:05
  • Lambda and method reference takes a little getting used to but it saves a lot of boiler plate, I updated my answer with an example using Java 7 syntax. Lambda and method reference takes a little getting used to but it saves a lot of boiler plate. – ayvazj Jan 08 '17 at 21:58
  • Thank you for the code :) But now I get `STARTED RxBleRadioOperationConnect` and after that `onConnectionStateChange newState=0 status=133` and everything is on the UI Thread... Any ideas for this? Is it in some way possible to set `TRANSPORT_BREDR` as the transport method? – Cilenco Jan 08 '17 at 22:43
  • status=133 is a general error from the native (C/C++) BLE stack. It is most often caused by not closing open BLE connections which leaves you in a state where you can not longer open a connection because you've maxed out your connection limits. – ayvazj Oct 28 '17 at 07:19
  • You don't necessarily need to run it on a UI thread, you just need all the calls to be on the SAME thread. I have crete an executor Thread and let all the callbacks\etc run from it. works like a charm :) – Re'em Jan 18 '18 at 14:22
9

Something that has been really useful for me is to wait for about 600ms after the connection has been established and then start the service discovery.

Roberto Betancourt
  • 2,375
  • 3
  • 27
  • 35
  • 1
    Wow that worked! Thank you `onServicesDiscovered` is now called but if I call `gatt.getServices()` it returns an empty list. Any ideas why? – Cilenco Jan 07 '17 at 21:40
  • 1
    @Cilenco you also have to verify that the service discovery was actually successful by checking that status == GATT_SUCCESS. Check this for more info: https://developer.android.com/reference/android/bluetooth/BluetoothGattCallback.html#onServicesDiscovered(android.bluetooth.BluetoothGatt, int). If it hasn't, then maybe you need to wait for longer before starting the service discovery. If it still doesn't work, then maybe your peripheral has other problems. – Roberto Betancourt Jan 09 '17 at 02:07
1

Had the same problem, but waiting 600 ms wasn't enough. This is probably due to the BLE module used. I fixed the problem by calling my method

discoverServices(); 

after calling

device.connectGatt(this,false,gattCallback)

I'm basically just calling discoverServices every 5 seconds (this is arbitrarily chosen)

private void discoverServices() {
    if(!gattConnected) { //just a boolean
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                gatt.discoverServices();
            }
        });
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                discoverServices();
            }
        }, 5000);
    }
}

In the onServicesDiscovered(...) method of my gattCallback I make gattConnected true. This worked for me.

AonAbiq
  • 11
  • 1
  • Reading all answers above you can be surprised how they even worked for someone. This answer is the only one that gives you a fallback solution if services can't be found after the first try. – Vlad Hudnitsky May 07 '20 at 17:13
0

This may help

I will explain in two steps: connecting and discovering services

connecting: connect from mainthread

set auto-reconnect to false

if version greater than or equals to M, set Transport type

else directly use reflection and handle it properly

Handler(applicationContext.mainLooper).post {
                    Log.d(TAG, " Post is called inside mainlooper")
                    mBluetoothGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        Log.d(TAG, " Is Or Greater than M $mBluetoothDevice")
                        mBluetoothDevice!!.connectGatt(this, false,
                                onGhattListener, TRANSPORT_LE)
                    } else {
                        Log.d(TAG, " Less than M")
                        try {
                            Log.d(TAG, " Trying TRANPORT LE with reflection")
                            val m = mBluetoothDevice!!.javaClass.getDeclaredMethod("connectGatt", Context::class.java, Boolean::class.javaPrimitiveType, BluetoothGattCallback::class.java, Int::class.javaPrimitiveType)
                            m.isAccessible = true
                            val transport = mBluetoothDevice!!.javaClass.getDeclaredField("TRANSPORT_LE").getInt(null)
                            m.invoke(mBluetoothDevice, this, false, onGhattListener, transport) as BluetoothGatt
                        } catch (e: Exception) {
                            e.printStackTrace()
                            Log.d(TAG, " Catch to call normal connection")
                            mBluetoothDevice!!.connectGatt(this, false,
                                    onGhattListener)
                        }
                    }
                    Log.d(TAG, "mBluetooth gatt is $mBluetoothGatt")
                    mBluetoothGatt?.let {
                        refreshDeviceCache(mBluetoothGatt!!)
                    }
                }

discover services : in onGhattListener , if device is connected fire discoverServices() from main thread

override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int,
                                         newState: Int) {
        Log.d(TAG, "onConnectionStateChange $gatt and status $status and newstate $newState")
        when (newState) {
            BluetoothGatt.STATE_CONNECTED -> {
                Handler(Looper.getMainLooper()).post {
                    gatt.discoverServices()
                }
            }
            BluetoothGatt.STATE_DISCONNECTED -> {

            }
            BluetoothGatt.STATE_CONNECTING -> {

            }
            BluetoothGatt.STATE_DISCONNECTING -> {

            }
        }
    }

this may solve your problem

call refresh method with reflection

fun refreshDeviceCache(gatt: BluetoothGatt): Boolean {
    try {
        val localMethod = gatt.javaClass.getMethod("refresh")
        if (localMethod != null) {
            return localMethod.invoke(gatt) as Boolean
        }
    } catch (localException: Exception) {
        Log.e(TAG, "An exception occured while refreshing device")
        localException.printStackTrace()
    }
    return false
}
Daniele
  • 668
  • 2
  • 10
  • 25
Balu Sangem
  • 712
  • 4
  • 16
0

I have also face the same issue. Start scanning in the onStart() method. It worked for me

Earlier I was starting the scan on onCreate method. That's why it did not work for me

-1

In fact,I solve this problem by Execution the method mBluetoothGatt.discoverServices() several times(10 or more),

    int i = 10;
    while (i > 0)
    {
        if (!mIsBLE_Finded)  //如果服务发现失败,继续执行discoverServices方法
        {
            i--;
            mBluetoothGatt.discoverServices();
            System.out.println("BLEService-->" + "尝试次数:" + i);
        }
        else //在10次的尝试中,存在某次服务发现成功了
        {
            i = -1;
        }
    }
Vihan
  • 1
  • 1