This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Connection between Android phone and nRF52

Hello,

I'm having problems getting a stable channel from an Android device (LG K8) and the nRF52 development board. It is hard to tell whether which side is causing the problems. What happens is that one of the side believes that the data is sent, but it seems like it wasn't sent. I have seen this problem from both sides. We are simulating a UART over BLE (very similar to the nordic UART profile). I.e., we are using notifications and write without response.

The channel can be stable, and then it stops working and will not start work again before Bluetooth is restarted on the Android device. When investigating this i decided to enable the HCI snoop log and i can find some interesting stuff here.

Some notes about the android code:

  • I always wait for the corresponding gatt callback before performing a new gatt command.
  • The callbacks does not block but instead adds the next task to be handled by another thread. Inspiration from this document: droidcon.de/.../practical_bluetooth_le_on_android_0.pdf
  • Scanning is disabled when a connection is to be initiated.

This is the log from the application:

09-06 13:45:21.222 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_CONNECT
09-06 13:45:21.222 9507-11481/test.app D/BluetoothGatt: connect() - device: F6:C5:9D:24:12:0A, auto: false
09-06 13:45:21.222 9507-11481/test.app D/BluetoothGatt: registerApp()
09-06 13:45:21.222 9507-11481/test.app D/BluetoothGatt: registerApp() - UUID=78c2d4f1-e459-456f-993f-9e2667ad5c95
09-06 13:45:21.223 9507-9616/test.app D/BluetoothGatt: onClientRegistered() - status=0 clientIf=5
09-06 13:45:21.548 9507-9519/test.app D/BluetoothGatt: onClientConnectionState() - status=0 clientIf=5 device=F6:C5:9D:24:12:0A
09-06 13:45:21.548 9507-9519/test.app I/BleClientChannel: onConnectionStateChange, status 0, newState 2
09-06 13:45:21.548 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_NEGOTIATE_MTU
09-06 13:45:21.549 9507-11481/test.app D/BluetoothGatt: configureMTU() - device: F6:C5:9D:24:12:0A mtu: 247
09-06 13:45:21.784 9507-9518/test.app D/BluetoothGatt: onConfigureMTU() - Device=F6:C5:9D:24:12:0A mtu=247 status=0
09-06 13:45:21.784 9507-9518/test.app I/BleClientChannel: onMtuChanged, status: 0, mtu: 247
09-06 13:45:21.784 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_DISCOVER_SERVICES
09-06 13:45:21.784 9507-11481/test.app D/BluetoothGatt: discoverServices() - device: F6:C5:9D:24:12:0A
09-06 13:45:21.793 9507-9519/test.app D/BluetoothGatt: onSearchComplete() = Device=F6:C5:9D:24:12:0A Status=0
09-06 13:45:21.793 9507-9519/test.app I/BleClientChannel: onServicesDiscovered, status: 0
09-06 13:45:21.793 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_VERIFY_SERVICES
09-06 13:45:21.794 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_ENABLE_NOTIFICATIONS
09-06 13:45:21.794 9507-11481/test.app D/BluetoothGatt: setCharacteristicNotification() - uuid: 48f058af-2d79-41af-be4f-722dee1ab571 enable: true
09-06 13:45:21.834 9507-9518/test.app I/BleClientChannel: onDescriptorWrite, status: 0
09-06 13:45:21.835 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_NOTIFICATIONS_ENABLED
09-06 13:45:21.840 9507-11134/test.app I/BleClientChannel: Writing
09-06 13:45:21.840 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_SEND_DATA
09-06 13:45:21.843 9507-9616/test.app I/BleClientChannel: onCharacteristicWrite, status: 0
09-06 13:45:21.843 9507-9616/test.app I/BleClientChannel: cha.getValue(): 2a00000053437632010000000000e87ac51c25f2c15f044bb9609eac34b42341b35ca5ac1244806a544345f47f7c
09-06 13:45:21.843 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_DATA_SENT
09-06 13:45:21.844 9507-11134/test.app I/BleClientChannel: write done
09-06 13:45:21.844 9507-11134/test.app I/BleClientChannel: read enter
09-06 13:45:21.983 9507-9616/test.app I/BleClientChannel: onCharacteristicChanged
09-06 13:45:21.983 9507-9616/test.app I/BleClientChannel: cha.getValue(): 26000000020000000000b54cda9af965c35188cf9fed036eef70e36523f8696b9daa4e22f2fb6e039d05
09-06 13:45:21.984 9507-11134/test.app I/BleClientChannel: Read done
09-06 13:45:21.986 9507-11134/test.app I/BleClientChannel: read enter
09-06 13:45:21.987 9507-9518/test.app I/BleClientChannel: onCharacteristicChanged
09-06 13:45:21.987 9507-9518/test.app I/BleClientChannel: cha.getValue(): 7800000006004f79e270c2d317c7eecc2fb6e720175e9360d887a0b189823596306b3d65de2a7a7424a015664f527568807b492a6a43e123a4b9aa379eae94f9c01be289c172bc6fdd857f1c452971d644b10d0f309c3368f8c0d015e3d5d7a987041cec70e00656ccdfba6775d4eeadae2529c9b7ae5ce37b21cd67
09-06 13:45:21.987 9507-11134/test.app I/BleClientChannel: Read done
09-06 13:45:21.990 9507-11134/test.app I/BleClientChannel: Writing
09-06 13:45:21.990 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_SEND_DATA
09-06 13:45:21.991 9507-9616/test.app I/BleClientChannel: onCharacteristicWrite, status: 0
09-06 13:45:21.991 9507-9616/test.app I/BleClientChannel: cha.getValue(): 780000000600ec761d5f10f6263afba2242666bd115f454e61881215b333747074974edef3d85f1f75c93d112d681fa5b6278ff8f1b49827e398f358b248dce414cd4ee3c91c5a19b54ef38f5aa94cfb4ab48359cb7ba808b3f7ea3a09af2cded0a79f26902e661b124432b0fc97cc173b756bd71f84e483eaf4c204
09-06 13:45:21.992 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_DATA_SENT
09-06 13:45:21.992 9507-11134/test.app I/BleClientChannel: write done
09-06 13:45:21.993 9507-11496/test.app I/BleClientChannel: read enter
09-06 13:45:21.993 9507-11497/test.app I/BleClientChannel: Writing
09-06 13:45:21.993 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_SEND_DATA
09-06 13:45:21.996 9507-9518/test.app I/BleClientChannel: onCharacteristicWrite, status: 0
09-06 13:45:21.996 9507-9518/test.app I/BleClientChannel: cha.getValue(): 3c0000000600caf11f37ad3099a41c70445fd69f035ded8e2edb4507fce5c30de5ace5de4480731edaee8f9f5ec91162933e60b25c5245592ff55af8aed4f11d
09-06 13:45:21.996 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_DATA_SENT
09-06 13:45:21.999 9507-11497/test.app I/BleClientChannel: write done
09-06 13:45:25.993 9507-11134/test.app I/BleClientChannel: close enter
09-06 13:45:25.993 9507-11134/test.app I/BleClientChannel: Close handeled
09-06 13:45:25.993 9507-11481/test.app I/BleClientChannel: task to handle: GATT_TASK_DISCONNECT
09-06 13:45:25.993 9507-11481/test.app D/BluetoothGatt: cancelOpen() - device: F6:C5:9D:24:12:0A
09-06 13:45:25.998 9507-9519/test.app D/BluetoothGatt: onClientConnectionState() - status=0 clientIf=5 device=F6:C5:9D:24:12:0A
09-06 13:45:25.998 9507-9519/test.app I/BleClientChannel: onConnectionStateChange, status 0, newState 0

The connection seems to be broken after the second onCharacteristicChanged callback. However, no gatt callback are received here and gatt.writeCharateristics does not return any error. Further, the onCharacteristicWrite are recevied. After his i try to send more data. None of these packages are received on the nRF52 side.

When i pulled the HCI snoop log i can only see events until the last onCharacteristicChanged. The next after this is a disconnect that is sent from the host to the controller. This happends when there is no response in 4 seconds, then we disconnect and consider this as a timeout.

Here is a copy from this session when opening the HCI snoop log with wireshark.

4204    132.134644  controller  HCI_CMD 6   host    Sent LE Set Scan Enable
4205    132.139460  host    HCI_EVT 7   controller  Rcvd Command Complete (LE Set Scan Enable)
4206    132.139506  controller  HCI_CMD 11  host    Sent LE Set Scan Parameters
4207    132.176921  host    HCI_EVT 7   controller  Rcvd Command Complete (LE Set Scan Parameters)
4208    132.177347  controller  HCI_CMD 7   host    Sent Vendor Command 0x0157 (opcode 0xFD57)
4209    132.179084  host    HCI_EVT 10  controller  Rcvd Command Complete (Vendor Command 0x0157 [opcode 0xFD57])
4210    132.186310  controller  HCI_CMD 29  host    Sent LE Create Connection
4211    132.187865  host    HCI_EVT 7   controller  Rcvd Command Status (LE Create Connection)
4212    132.463131  host    HCI_EVT 22  controller  Rcvd LE Meta (LE Connection Complete)
4213    132.463485  controller  HCI_CMD 6   host    Sent LE Read Remote Used Features
4214    132.465962  host    HCI_EVT 7   controller  Rcvd Command Status (LE Read Remote Used Features)
4215    132.494882  LgElectr_2e:f2:11 (LG K8 4G)    ATT 12  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd Exchange MTU Request, Client Rx MTU: 247
4216    132.496022  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    ATT 12  LgElectr_2e:f2:11 (LG K8 4G)    Sent Exchange MTU Response, Server Rx MTU: 247
4217    132.595092  host    HCI_EVT 12  controller  Rcvd Number of Completed Packets
4218    132.595628  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    ATT 12  LgElectr_2e:f2:11 (LG K8 4G)    Sent Exchange MTU Request, Client Rx MTU: 247
4219    132.744603  LgElectr_2e:f2:11 (LG K8 4G)    ATT 12  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd Exchange MTU Response, Server Rx MTU: 247
4220    132.759773  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    ATT 14  LgElectr_2e:f2:11 (LG K8 4G)    Sent Write Request, Handle: 0x0013 (Unknown: Unknown: Client Characteristic Configuration)
4221    132.795599  LgElectr_2e:f2:11 (LG K8 4G)    ATT 10  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd Write Response, Handle: 0x0013 (Unknown: Unknown: Client Characteristic Configuration)
4222    132.797135  host    HCI_EVT 12  controller  Rcvd Number of Completed Packets
4223    132.804275  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    ATT 58  LgElectr_2e:f2:11 (LG K8 4G)    Sent Write Command, Handle: 0x0010 (Unknown: Unknown)
4224    132.944446  LgElectr_2e:f2:11 (LG K8 4G)    HCI_ACL 32  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd  [Reassembled in #4225]
4225    132.945044  LgElectr_2e:f2:11 (LG K8 4G)    ATT 27  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd Handle Value Notification, Handle: 0x0012 (Unknown: Unknown)
4226    132.946419  LgElectr_2e:f2:11 (LG K8 4G)    HCI_ACL 32  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd  [Reassembled in #4230]
4227    132.946525  LgElectr_2e:f2:11 (LG K8 4G)    HCI_ACL 32  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd  [Continuation to #4226]
4228    132.947045  LgElectr_2e:f2:11 (LG K8 4G)    HCI_ACL 32  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd  [Continuation to #4226]
4229    132.948283  LgElectr_2e:f2:11 (LG K8 4G)    HCI_ACL 32  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd  [Continuation to #4226]
4230    132.948706  LgElectr_2e:f2:11 (LG K8 4G)    ATT 28  f6:c5:9d:24:12:0a (PoTAa69db5d5LOCK)    Rcvd Handle Value Notification, Handle: 0x0012 (Unknown: Unknown)
4231    136.956180  controller  HCI_CMD 7   host    Sent Disconnect
4232    136.959145  host    HCI_EVT 7   controller  Rcvd Command Status (Disconnect)
4233    136.993523  controller  HCI_CMD 6   host    Sent Vendor Command 0x0157 (opcode 0xFD57)
4234    137.000253  host    HCI_EVT 7   controller  Rcvd Disconnect Complete
4235    137.000316  host    HCI_EVT 9   controller  Rcvd Command Complete (Vendor Command 0x0157 [opcode 0xFD57])

So, it appears that all gatt commands after the last onCharacteristicChanged callback are ignored. So, my guess is that this is an android issue. However, i need to know how i can approach this issue, where do i start?

Here is the android code:

package test.androidlib;

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.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;


import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;

import test.common.ToWaitFor;
import test.common.cut.Hex;
import saltchannel.ByteChannel;
import saltchannel.ComException;
import saltchannel.StreamChannel;

/**
 * The client-side of a BLE channel session.
 *
 */
public class BleClientChannel implements ByteChannel, Handler.Callback {

    private final String TAG = "BleClientChannel";
    private static final int MAX_NUM_CONNECTION_RETRIES = 0;
    private static final int MAX_MTU_SIZE = 247;

    private Context context;
    private LogOutput logOutput;
    private Listener listener;
    private long timeout = 6000;
    private final Object stateLock = new Object();

    private final AtomicBoolean connectCalled = new AtomicBoolean(false);
    private final AtomicBoolean isClosed = new AtomicBoolean(false);
    private HandlerThread handlerThread;
    private Handler bleHandler;
    private BluetoothDevice mDevice;
    private BluetoothGatt mGatt;
    private BluetoothGattCharacteristic inCha;
    private BluetoothGattCharacteristic outCha;

    private int effectiveMtu = 20;
    private int connectionRetries = 0;
    private GattEvent connectedEvent;
    private GattEvent writeEvent;
    private GattEvent disconnectedEvent;
    private byte[] nextPacketToSend;

    private CircularByteBuffer circularByteBuffer;
    private ByteChannel byteChannel;
    private OutputStream bleOutputStream;
    private BluetoothManager manager;

    public BleClientChannel(Context context) {
        this.context = context;
        this.logOutput = new LogOutput() {

            @Override
            public void print(String s) {
                Log.i(TAG, s);
            }
        };
        this.listener = new Listener() {

            @Override
            public void disconnected() {
                log("Disconnected");
            }
        };

        this.handlerThread = new HandlerThread("BLE-Worker");

        manager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);


    }

    public void initLog(LogOutput logOutput) {
        this.logOutput = logOutput;
    }

    public void initListener(BleClientChannel.Listener listener) {
        this.listener = listener;
    }

    public void initTimeout(int millis) {
        this.timeout = millis;
    }

    /**
     * Connects to the device.
     *
     * @param device The BLE device to connect to.
     * @throws IllegalStateException if a connect attempt has been done with this object already.
     * @throws IOException if the connection could not be established.
     *
     */
    public void connect(BluetoothDevice device) throws IOException {


        if (connectCalled.get()) {
            connectCalled.set(true);
            throw new IllegalStateException("connect() have already been called");
        }

        Log.i("BleClientChannel", "Connected devices");
        for (BluetoothDevice tmpDevice : manager.getConnectedDevices(BluetoothProfile.GATT)) {
            // TODO: Verify not connected already
            Log.i(TAG, tmpDevice.getAddress());
        }


        this.mDevice = device;
        connectedEvent = new GattEvent();

        // Start BLE Worker thread
        handlerThread.start();
        bleHandler = new Handler(handlerThread.getLooper(), this);
        addTask(MyGattTask.GattTask.GATT_TASK_CONNECT);

        connectedEvent.waitForIt(timeout);

        if (!connectedEvent.isOk) {
            closeHard(true);
            throw new IOException(connectedEvent.message);
        }

        connectedEvent = null;

        this.circularByteBuffer = new CircularByteBuffer(100*1000, false);
        createBleStreamChannel();

    }

    public void close() {

        log("close enter");

        if (isClosed.get()) {
            isClosed.set(true);
            return;
        }

        log("Close handeled");

        if (connectedEvent != null) {
            connectedEvent.isOk = false;
            connectedEvent.message = "Canceled by user";
            connectedEvent.reportHappened();
        }

        synchronized (stateLock) {
        if (bleHandler != null) {

            disconnectedEvent = new GattEvent();
            bleHandler.removeMessages(0);
            addTask(MyGattTask.GattTask.GATT_TASK_DISCONNECT);
            disconnectedEvent.waitForIt(200);

        }
        }

        closeHard(false);

        log("close done");

    }

    private void closeHard(boolean reallyHard) {

        log("Closing hard, reallyHard: " + reallyHard);

        if (reallyHard) {
            if (isClosed.get()) {
                isClosed.set(true);
                return;
            }
        }

        synchronized (stateLock) {
            if (bleHandler != null) {
                bleHandler.removeCallbacksAndMessages(null);
                bleHandler.getLooper().quit();
                bleHandler = null;
            }

            if (this.mGatt != null) {
                this.mGatt.disconnect();
                this.mGatt.close();
                this.mGatt = null;
                new Thread(new Runnable() {

                    @Override
                    public void run() {
                        listener.disconnected();
                    }
                }).start();
            }

            if (byteChannel != null) {
                try {
                    circularByteBuffer.getInputStream().close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    private void addTask(MyGattTask.GattTask task) {

        if (bleHandler != null) {
            if (bleHandler.getLooper().getThread().isAlive()) {
                Message msg = bleHandler.obtainMessage();
                msg.what = 0;
                msg.obj = new MyGattTask(task);
                msg.sendToTarget();
            }
        }
    }

    private void handleInternalError(final String error) {

        log("Internal error: " + error);

        /*
         * This function is called from the bluetoothGattCallback, due to the syncronized block
         * this method might block for a while. One should never block bluetoothGattCallback.
         * Therefore, a new thread/runnable is started.
         */
        new Thread(new Runnable() {

            @Override
            public void run() {
                synchronized (stateLock) {
                    if (writeEvent != null) {
                        writeEvent.isOk = false;
                        writeEvent.message = error;
                        writeEvent.reportHappened();
                    }
                    if (connectedEvent != null) {
                        connectedEvent.message = error;
                        connectedEvent.isOk = false;
                        connectedEvent.reportHappened();
                    }
                    closeHard(true);
                }
            }
        }).start();

    }

    @Override
    public byte[] read() throws ComException {
        log("read enter");

        if (isClosed.get()) {
            throw new ComException("Channel closed");
        }

        if (this.byteChannel != null) {
            byte[] tmp = this.byteChannel.read();
            log("Read done");
            return tmp;
        } else {
            throw new ComException("No byte channel available.");
        }

    }

    @Override
    public void write(byte[]... bytes) throws ComException {
        if (this.byteChannel != null) {
            this.byteChannel.write(bytes);
        } else {
            throw new ComException("No byte channel available.");
        }
    }

    @Override
    public boolean handleMessage(Message msg) {

        boolean ok = true;
        String errorMsg = "";
        MyGattTask.GattTask task = ((MyGattTask) msg.obj).task;

        log("task to handle: " + task);

        switch (task) {
            case GATT_TASK_CONNECT:
                this.mGatt = this.mDevice.connectGatt(this.context, false, mGattCallback);
                break;
            case GATT_TASK_RETRY_CONNECT:
                this.mGatt.close();
                this.mGatt = this.mDevice.connectGatt(this.context, false, mGattCallback);
                break;
            case GATT_TASK_NEGOTIATE_MTU:
                ok = this.mGatt.requestMtu(MAX_MTU_SIZE);
                errorMsg = "requestMtu() unexpectedly returned false";
                break;
            case GATT_TASK_DISCOVER_SERVICES:
                ok = this.mGatt.discoverServices();
                errorMsg = "discoverServices() unexpectedly returned false";
                break;
            case GATT_TASK_VERIFY_SERVICES:
                try {
                    setupTestService();
                    addTask(MyGattTask.GattTask.GATT_TASK_ENABLE_NOTIFICATIONS);
                } catch (IllegalStateException e) {
                    ok = false;
                    errorMsg = e.toString();
                }
                break;
            case GATT_TASK_ENABLE_NOTIFICATIONS:
                try {
                    enableNotifications();
                } catch (IllegalStateException e) {
                    errorMsg = e.toString();
                    ok = false;
                }
                break;
            case GATT_TASK_NOTIFICATIONS_ENABLED:
                if (connectedEvent != null) {
                    connectedEvent.isOk = true;
                    connectedEvent.reportHappened();
                }
                break;
            case GATT_TASK_SEND_DATA:
                if (nextPacketToSend != null) {
                    inCha.setValue(nextPacketToSend);
                    ok = this.mGatt.writeCharacteristic(inCha);
                    errorMsg = "writeCharacteristic() unexpectedly returned false";
                }
                break;
            case GATT_TASK_DATA_SENT:
                if (writeEvent != null) {
                    writeEvent.isOk = true;
                    writeEvent.reportHappened();
                }
                break;
            case GATT_TASK_DISCONNECT:
                this.mGatt.disconnect();
                break;
            case GATT_TASK_DISCONNECTED:
                break;
            default:
                log("Unhandled event");
                break;
        }

        if (!ok) {
            handleInternalError(errorMsg);
        }

        return ok;
    }

    private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {

            log(String.format("onConnectionStateChange, status %d, newState %d", status, newState));


            if (status != BluetoothGatt.GATT_SUCCESS) {

                if (BleClientChannel.this.connectionRetries < MAX_NUM_CONNECTION_RETRIES && status == 133) {
                    // Status 133 seems to be recoverable, we try to reconnect here
                    BleClientChannel.this.connectionRetries++;
                    addTask(MyGattTask.GattTask.GATT_TASK_RETRY_CONNECT);
                } else {
                    handleInternalError("status != GATT_SUCCESS in onConnectionStateChange");
                }
                handleInternalError("status != GATT_SUCCESS in onConnectionStateChange");

                return;
            }

            if (newState == BluetoothProfile.STATE_CONNECTED) {
                addTask(MyGattTask.GattTask.GATT_TASK_NEGOTIATE_MTU);
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                if (disconnectedEvent != null) {
                    disconnectedEvent.isOk = true;
                    disconnectedEvent.reportHappened();
                }
                handleInternalError("Disconnected by peer");
            }
        }
        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
            log(String.format("onMtuChanged, status: %d, mtu: %d", status, mtu));
            if (status != BluetoothGatt.GATT_SUCCESS) {
                handleInternalError("status != GATT_SUCCESS in onMtuChanged");
                return;
            }

            if (mtu <= MAX_MTU_SIZE) {
                effectiveMtu = mtu - 3;
            } else {
                effectiveMtu = 23;
            }

            addTask(MyGattTask.GattTask.GATT_TASK_DISCOVER_SERVICES);

        }
        public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
            log(String.format("onServicesDiscovered, status: %d", status));
            if (status != BluetoothGatt.GATT_SUCCESS) {
                handleInternalError("status != GATT_SUCCESS in onServicesDiscovered");
                return;
            }
            addTask(MyGattTask.GattTask.GATT_TASK_VERIFY_SERVICES);

        }
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            log(String.format("onDescriptorWrite, status: %d", status));

            if (status != BluetoothGatt.GATT_SUCCESS) {
                handleInternalError("status != GATT_SUCCESS in onDescriptorWrite");
                return;
            }

            addTask(MyGattTask.GattTask.GATT_TASK_NOTIFICATIONS_ENABLED);
        }
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            // A message (chunk) was sent to the BLE device, now we can proceed sending next
            log(String.format("onCharacteristicWrite, status: %d", status));
            log("cha.getValue(): " + Hex.create(characteristic.getValue()));
            if (status != BluetoothGatt.GATT_SUCCESS) {
                handleInternalError("status != GATT_SUCCESS in onCharacteristicWrite");
                return;
            }
            addTask(MyGattTask.GattTask.GATT_TASK_DATA_SENT);
        }

        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic cha) {
            // Data was sent from BLE device
            log("onCharacteristicChanged");
            if (BleTestServer.OUT_UUID.equals(cha.getUuid())) {
                byte[] bytes = cha.getValue();
                log("cha.getValue(): " + Hex.create(bytes));
                try {
                    circularByteBuffer.getOutputStream().write(bytes, 0, bytes.length);
                } catch (IOException e) {
                    handleInternalError("onCharacteristicChanged, Could not add data to outputStream");
                }
            } else {
                log("onCharacteristicChanged, Unexpected characteristic");
            }
        }
    };

    private void enableNotifications() throws IllegalStateException {
        boolean ok;
        ok = this.mGatt.setCharacteristicNotification(outCha, true);

        if (!ok) {
            throw new IllegalStateException("gatt.setCharacteristicNotification unexpectedly returned false");
        }
        BluetoothGattDescriptor descriptor = outCha.getDescriptor(BleUtil.CLIENT_CHARACTERISTIC_CONFIG);
        ok = descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        if (!ok) {
            throw new IllegalStateException("descriptor.setValue() unexpectedly returned false");
        }

        ok = this.mGatt.writeDescriptor(descriptor);
        if (!ok) {
            throw new IllegalStateException("gatt.writeDescriptor() unexpectedly returned false");
        }
    }

    private void setupTestService() throws IllegalStateException {

        BluetoothGattService service = this.mGatt.getService(BleTestServer.SERVICE_UUID);
        if (service == null) {
            throw new IllegalStateException("No Test service found");
        }

        inCha(service);
        outCha(service);

    }

    private void inCha(BluetoothGattService service) throws IllegalStateException {
        inCha = service.getCharacteristic(BleTestServer.IN_UUID);

        if (inCha == null) {
            throw new IllegalStateException("No in characteristics found");
        }

        int properties = inCha.getProperties();

        if (properties != BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) {
            throw new IllegalStateException("unexpected in characteristics properties, " + properties);
        }
    }

    private void outCha(BluetoothGattService service) throws IllegalStateException {
        outCha = service.getCharacteristic(BleTestServer.OUT_UUID);
        if (outCha == null) {
            throw new IllegalStateException("No out characteristics found");
        }

        int properties = outCha.getProperties();
        if (properties != BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
            throw new IllegalStateException("unexpected outCha properties, " + properties);
        }

    }


    private void createBleStreamChannel() {
        createBleOutputStream();

        byteChannel = new StreamChannel(circularByteBuffer.getInputStream(), bleOutputStream);

    }

    private void createBleOutputStream() {
        bleOutputStream = new OutputStream() {

            @Override
            public void write(int oneByte) throws IOException {
                write(new byte[]{(byte) oneByte}, 0, 1);
            }

            public void write(byte[] bytes) throws IOException {
                write(bytes, 0, bytes.length);
            }

            public void write(byte[] bytes, int initialOffset, int length) throws IOException {

                int bytesLeft = length;
                int offset = initialOffset;

                byte[] tempBytes = new byte[bytes.length - 4];
                System.arraycopy(bytes, 4, tempBytes, 0, length-4);

                log("Writing");

                while (bytesLeft > 0) {

                    if (isClosed.get()) {
                        throw new IOException("BLE Output stream is closed");
                    }

                    int packetSize = effectiveMtu;
                    if (bytesLeft < effectiveMtu) {
                        packetSize = bytesLeft;
                    }

                    byte[] packet = new byte[packetSize];
                    System.arraycopy(bytes, offset, packet, 0, packetSize);

                    nextPacketToSend = packet;

                    writeEvent = new GattEvent();
                    addTask(MyGattTask.GattTask.GATT_TASK_SEND_DATA);
                    writeEvent.waitForIt(timeout);
                    if (writeEvent.hasHappened()) {
                        if (!writeEvent.isOk) {
                            throw new IOException(writeEvent.message);
                        }
                    }
                    writeEvent = null;

                    bytesLeft -= packetSize;
                    offset += packetSize;

                }

                log("write done");
            }
        };
    }

    private void log(String s) {
        this.logOutput.print(s);
    }

    /**
     * Event happened when channel has been established,
     * when a timeout occurred, or when an error occurred.
     */
    private static class GattEvent extends ToWaitFor {

        public String message = "";
        public String type = "";
        public boolean isOk = false;

        @Override
        public boolean waitForIt(long timeout) {
            boolean happened = super.waitForIt(timeout);
            if (!happened) {
                type = "timeout";
                message = "timeout occurred";
                isOk = false;
            }

            return happened;
        }
    }

    /**
     * Listens to event that happens to the BLE client.
     */
    public interface Listener {
        /**
         * Reports the event that the client was disconnected.
         */
        void disconnected();
    }
}

Any suggestions?

  • Does it happen 100% times? Have you tried another phone, perhaps with a never version of Android, or with a different chipset? Im my opinion it's something in LG's custom Android version. Have you tried sending data with smaller mtu, or just in a chain of 20 bytes withouts seeing the mtu at the beginning?

  • It does not happen 100% of the times. I don't know what event causes the this trouble to begin, but when it happens there is no turning back without restarting Bluetooth on the Android device. From the HCI log i could actually see that the nRF52 side in this case also request the MTU negotiation. However, i found no API in Android to see what the current maximum MTU is, therefore we also negotiate it. We talk to devices that supports only 23 (20 bytes effective data) and 247 (243 bytes effective data). I will test using only 20 bytes effective to see if this occurs. With other phones, this works better but sometimes we end up in this scenario also.

    Have you seen any of these issues with your applications with specific Android devices?

  • The case when the Bluetooth on a phone get into a state when it needs to be restarted (or, sometimes, the phone needs reset as the Bluetooth doesn't turn off) is caused by some leakage or similar error in the Android, or its ble stack. There is not much you can do. Perhaps adding some more delays here and there to give the adapter time to rethink its life... Gladly I didn't see this kind of problems or recent phones, that is Android 5 or maybe 6 or newer, or with newer chipsets, so in few years it will solve itself as the phone will be replaced. As I wrote, all you can do is try some workarounds, add delays, change mtu, etc. Or disable some known phones/tablets (Nexus 4 & 7) on Google play. The code you have should work, from what I see. Nothing wrong there.

  • Thanks for good comments. I tried reverting to an older version of our application, with this version everything is more stable even tough the code for the BLE connection is not changed. However, we have added other services (not related to BLE), that communicates with a server and some other tasks. There might be some threading issues with these services that causes the issue with the Bluetooth. I don't have a lot of knowledge how to communication between the application - android os - Bluetooth controller works, but perhaps some of our processes affects that.

  • We are using this BLE channel to simulate UART (very similar to nodric UART), on this channel we establish an encrypted channel similar to TLS. Between the first message received and the thirds message received we do some heavy crypto calculations (x25519 key agreement). This is done by a completely other thread so we do not block any gatt callbacks. However, do gain good performance on the crypto operations we use a library that is written in native C code (libsodium). Can the binding between java and the C code somehow affect the android OS handling of all BLE stuff?

Related