Android BluetoothGattServer hangs - client cannot read/write characteristics

Viewed 357

I'm developing a project that consists of a BLE GATT server ran on Android phone (using BluetoothGattServer Java class) and a BLE client on an IoT board. The concept of using the phone as the server is to somehow protect the IoT board from attacking clients. When my Android application wants to talk to the external device it starts advertising a special set of data and if the external device recognizes the advertisment it connects to the Android BLE GATT server. Then the external device reads the presented services and chareacteristics and registers for notifications on some of the chars. By far it all happens well.

After that the external device tries to write authentication data to one of the chars. If everything was clean and perfect, the process goes well. But if for a reason the last connection broke in the middle of some operation and was not properly closed, the external device cannot read/write the characteristic. If I restart the phone or clear Bluetooth cache (Settings > Apps > Bluetooth > Clear Data) all the operations proceed fine, but I cannot force users to do this in normal operation and I haven't found how and if I could clear this cache programmatically from inside the app.

  • I read over the web for GATT client cache undocumented "Refresh" method, but in GATT server there is no such.
  • I read about and tried the "Service changed" characteristic (0x2A05) but it doesn't help me much.
  • Having doubts about which of the devices is causing the problem I have tried with another phone as a client. I ran "BLE Scanner" App on it and tried to connect to the server phone - the problem persists. I can connect, all the characteristics are discovered, but when I try to read/write some char the connection brakes after a 30 sec timeout - exactly the same behavior like in the original situation. The conclusion is I have a problem with Android BluetoothGattServer.

During the development the problem occurs mostly when connection is broken by some error in communication. In real life usage after I have all errors fixed that will not happen, but having in mind tha it is wireless radio connection, it can be disconnected by literally everything and I shall have a reliable mechanism to reconnect.

I open the server with this code:

    private void startServer() {
        BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
        mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
        if (mBluetoothLeAdvertiser == null) {
            Log.w("BLE", "Failed to create advertiser");
            return;
        }

        AdvertiseSettings settings = new AdvertiseSettings.Builder()
                .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
                .setConnectable(true)
                .setTimeout(0)
                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
                .build();

        byte bData[] = new byte[24];
        bData = ...... // some proprietary advertising data
        AdvertiseData data = new AdvertiseData.Builder()
                .setIncludeDeviceName(false)
                .setIncludeTxPowerLevel(false)
                .addManufacturerData(iManufID, bData)
                .build();

        mBluetoothLeAdvertiser
                .startAdvertising(settings, data, mAdvertiseCallback);
        mBluetoothGattServer = mBluetoothManager.openGattServer(ctxActivity, mGattServerCallback);
        mBluetoothGattServer.clearServices();
        mBluetoothGattServer.addService(BLEProfile.createBLEService()); 
             /* Static method, which builds Service->Chars->Descriptors. 
              I assign the Client Config descriptor (0x2902) to each characteristic. */
    }

For stopping the sertver I use this code:

    private void stopServer() {
        if (mBluetoothGattServer == null) return;
        mBluetoothGattServer.clearServices();
        mBluetoothGattServer.close();

        if (mBluetoothLeAdvertiser == null) return;
        mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
    }

I stop and start the server each time a connection was broken.

Does anyone have an idea what am I doing wrong?

Also it is important to mention that in real life IoT devices will be many in a room, phones may be many in a room. One phone should be able to be connected by any of the IoT devices it requests sequentially and one IoT device should be able to connect to more than one phone sequentially. The advertising data of the phone's GATT Server defines which of the IoT devices is requested to connect and it will change each time the phone requests connection with a different device.

Update: Here is the code for the Server Callback:

    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onMtuChanged(BluetoothDevice device, int mtu) {
            super.onMtuChanged(device, mtu);
            Log.i("BLE", "MTU changed: "+mtu);
        }

        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            if(BLEProfile.XXXXXX.equals(characteristic.getUuid()))
            {
                // ... some data checks ...
                
                mBluetoothGattServer.sendResponse(device,
                        requestId,
                        iValid,
                        0,
                        bEncrypted);
                characteristic.setValue(bEncrypted);
                mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, true);
            } else if (...) /* similar operations for all other characteristics */
            {
                ...
            }
            else
            {
                Log.i("BLE", "Write not mine characteristic");
                mBluetoothGattServer.sendResponse(device,
                        requestId,
                        BluetoothGatt.GATT_FAILURE,
                        0,
                        null);
            }
        }

        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {

            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.i("BLE", "BluetoothDevice ... CONNECTED: " + device);
                if(device != null)
                {
                    BluetoothGattService mServ = mBluetoothGattServer.getService(BLEProfile.GATT_SERVICE);
                    if(mServ != null)
                    {
                        BluetoothGattCharacteristic mChar = mServ.getCharacteristic(BLEProfile.SERVICE_CHANGED);
                        if(mChar != null)
                            mBluetoothGattServer.notifyCharacteristicChanged(device, mChar, false);
                    }
                }
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                mRegisteredDevices.remove(device);
                stopAdvertising();
                startAdvertising();
            }
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
                                                BluetoothGattCharacteristic characteristic) {

            /* Not used currently. Just some testing code below. */
            if(BLEProfile.XXXXXX.equals(characteristic.getUuid()))
            {
                mBluetoothGattServer.sendResponse(device,
                    requestId,
                    BluetoothGatt.GATT_SUCCESS,
                    0,
                    new byte[] {0x01, 0x02, 0x03, 0x04, 0x05});
            } else if(...)      /* similar operations for all other characteristics */
            {
                ...
            }
            else
            {
                Log.i("BLE", "Read not mine characteristic");
                mBluetoothGattServer.sendResponse(device,
                    requestId,
                    BluetoothGatt.GATT_FAILURE,
                    0,
                    null);
            }
        }

        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
                                            BluetoothGattDescriptor descriptor) {

            if (BLEProfile.CLIENT_CONFIG.equals(descriptor.getUuid())) {
                Log.d("BLE", "Config descriptor read");
                byte[] returnValue;
                if (mRegisteredDevices.contains(device)) {
                    returnValue = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
                } else {
                    returnValue = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
                }
                /* Not sure why I am responding with GATT_FAILURE here insted of GAT_SUCCESS !?!? May be some copy/paste mistake. */
                mBluetoothGattServer.sendResponse(device,
                        requestId,
                        BluetoothGatt.GATT_FAILURE,
                        0,
                        returnValue);
            } else {
                Log.w("BLE", "Unknown descriptor read request");
                mBluetoothGattServer.sendResponse(device,
                        requestId,
                        BluetoothGatt.GATT_FAILURE,
                        0,
                        null);
            }
        }

        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
                                             BluetoothGattDescriptor descriptor,
                                             boolean preparedWrite, boolean responseNeeded,
                                             int offset, byte[] value) {

            if (BLEProfile.CLIENT_CONFIG.equals(descriptor.getUuid())) {
                if (Arrays.equals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, value)) {
                    Log.i("BLE", "Subscribe device to notifications: " + device);
                    mRegisteredDevices.add(device);
                } else if (Arrays.equals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, value)) {
                    Log.i("BLE", "Unsubscribe device from notifications: " + device);
                    mRegisteredDevices.remove(device);
                }
                if (responseNeeded) {
                    mBluetoothGattServer.sendResponse(device,
                            requestId,
                            BluetoothGatt.GATT_SUCCESS,
                            0,
                            null);
                }
            } else {
                Log.w("BLE", "Unknown descriptor write request");
                if (responseNeeded) {
                    mBluetoothGattServer.sendResponse(device,
                            requestId,
                            BluetoothGatt.GATT_FAILURE,
                            0,
                            null);
                }
            }
        }
    };
0 Answers
Related