Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android BLE notifications packet loss

I am working on a hardware device which sends a continuous stream of data over BLE to an Android App. The android app receives this data as GATT notifications, then processes this data and saves it to a database.

The configuration details of the project are as follows:

  1. Phone - Moto E 1st Generation
  2. Android Version - Android 5.1 - Lollipop 
  3. iOS - iPhone 4 & 5, tested on iOS 7 & 8
  4. Hardware - CC2541
  5. Connection_Interval : 40 ms (Set in the firmware of the hardware).
  6. Packets Sent Per Connection Interval : 4 (Set in the firmware of the hardware).

THE PROBLEM

When data is transmitted from the hardware device to the BLE data capture app running on Android phone, all the data packets are not received. It receives only about 35-45 packets, whereas the expected number of packets is 50.

What is more surprising is that when we used the BLE packet sniffer, there was a perfect match between the data sniffed and displayed by the Android phone (which is incomplete / incorrect data). This leads me to believe that the hardware is behaving differently when connecting to the Android phone and not sending all of the data.

When we use the same hardware with an iOS BLE data capture app, the data is received correctly.

I am puzzled and clueless about this behaviour of BLE data capture in Android. How the app on iOS device is able to capture all the data correctly whereas app on android phone is not able to capture data properly at all? 

Has anyone experienced such a problem of packet loss / incorrect data when using BLE app on Android?  Any inputs are welcome. Thank you so much for your help in advance.

The android App is using the standard BLE code to connect to the device over BLE. The Android code that I am using is shown below:

import android.annotation.TargetApi;
import android.app.Activity;
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.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.nfc.Tag;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

@TargetApi(21)
public class BLETestActivity extends Activity {
    private BluetoothAdapter mBluetoothAdapter;
    private int REQUEST_ENABLE_BT = 1;
    private Handler mHandler;
    private static final long SCAN_PERIOD = 10000;
    private BluetoothLeScanner mLEScanner;
    private ScanSettings settings;
    private List<ScanFilter> filters;
    private BluetoothGatt mGatt;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bletest);
        mHandler = new Handler();
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Toast.makeText(this, "BLE Not Supported",
                    Toast.LENGTH_SHORT).show();
            finish();
        }
        final BluetoothManager bluetoothManager =
                (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBluetoothAdapter = bluetoothManager.getAdapter();

    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
        } else {
            if (Build.VERSION.SDK_INT >= 21) {
                Log.i("innnnn","spinnnn - " + Build.VERSION.SDK_INT);
                mLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
                settings = new ScanSettings.Builder()
                        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                        .build();
                filters = new ArrayList<ScanFilter>();
            }
            scanLeDevice(true);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) {
            scanLeDevice(false);
        }
    }

    @Override
    protected void onDestroy() {
        if (mGatt == null) {
            return;
        }
        mGatt.close();
        mGatt = null;
        super.onDestroy();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_ENABLE_BT) {
            if (resultCode == Activity.RESULT_CANCELED) {
                //Bluetooth not enabled.
                finish();
                return;
            }
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

    private void scanLeDevice(final boolean enable) {
        if (enable) {
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (Build.VERSION.SDK_INT < 21) {
                        mBluetoothAdapter.stopLeScan(mLeScanCallback);
                    } else {
                        mLEScanner.stopScan(mScanCallback);

                    }
                }
            }, SCAN_PERIOD);
            if (Build.VERSION.SDK_INT < 21) {
                mBluetoothAdapter.startLeScan(mLeScanCallback);
            } else {
                mLEScanner.startScan(filters, settings, mScanCallback);
            }
        } else {
            if (Build.VERSION.SDK_INT < 21) {
                mBluetoothAdapter.stopLeScan(mLeScanCallback);
            } else {
                mLEScanner.stopScan(mScanCallback);
            }
        }
    }


    private ScanCallback mScanCallback = null;/* new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            Log.i("callbackType", String.valueOf(callbackType));
            Log.i("result", result.toString());
            BluetoothDevice btDevice = result.getDevice();
            connectToDevice(btDevice);
        }

        @Override
        public void onBatchScanResults(List<ScanResult> results) {
            for (ScanResult sr : results) {
                Log.i("ScanResult - Results", sr.toString());
            }
        }

        @Override
        public void onScanFailed(int errorCode) {
            Log.e("Scan Failed", "Error Code: " + errorCode);
        }
    };*/

    private BluetoothAdapter.LeScanCallback mLeScanCallback =
            new BluetoothAdapter.LeScanCallback() {
                @Override
                public void onLeScan(final BluetoothDevice device, int rssi,
                                     byte[] scanRecord) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Log.i("onLeScan", device.toString());
                            connectToDevice(device);
                        }
                    });
                }
            };

    public void connectToDevice(BluetoothDevice device) {
        if (mGatt == null) {
            mGatt = device.connectGatt(this, false, gattCallback);
            scanLeDevice(false);// will stop after first device detection
        }
    }

    private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            Log.i("onConnectionStateChange", "Status: " + status);
            switch (newState) {
                case BluetoothProfile.STATE_CONNECTED:
                    Log.i("gattCallback", "STATE_CONNECTED");
                    gatt.discoverServices();
                    break;
                case BluetoothProfile.STATE_DISCONNECTED:
                    Log.e("gattCallback", "STATE_DISCONNECTED");
                    break;
                default:
                    Log.e("gattCallback", "STATE_OTHER");
            }

        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            List<BluetoothGattService> services = gatt.getServices();
           // Log.i("onServicesDiscovered", services.toString());

            for(int i=0;i<services.size();i++) {
                List<BluetoothGattCharacteristic> charList = services.get(i).getCharacteristics();
                for (int j = 0; j < charList.size();j++) {
                    BluetoothGattCharacteristic characteristic = charList.get(j);
                    if (characteristic.getUuid().compareTo(Constants.HEART_DATA_UUID) == 0) {
                        Log.i(BLETestActivity.class.getSimpleName(), "Characteristic UUID is " + characteristic.getUuid());
                        mGatt.setCharacteristicNotification(characteristic, true);
                        BluetoothGattDescriptor descriptor = characteristic.getDescriptor(Constants.HEART_DATA_DESCRIPTOR_UUID);
                        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                        mGatt.writeDescriptor(descriptor);
                    }
                }
            }
        }

        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic) {
            // Retrieve the byte values
            // Convert them to hex values
            byte[] data = characteristic.getValue();

            // The following array contains data in the follwing form:
            // 0-1 - running counter counts
            // 2-4, 5-7, 8-10  - Sample 1 data for channel 1, channel2 and channel 3
            // 11-13, 14-16, 17-19 - Sample 2 data for channel 1, channel 2 and channel 3

//            Log.i(TAG," ------ " + Thread.currentThread().getId());
            String heartBeatDataInHex = bytesToHex(data);


            // An error packet is received after every 17 samples.
            // Checking to make sure that this is not an error packet

            if (!(heartBeatDataInHex.substring(4, 10).equalsIgnoreCase("FFFFFF") && heartBeatDataInHex.substring(26, 40).equalsIgnoreCase("FFFFFFFFFFFFFF"))) {
                Log.i("testdata", heartBeatDataInHex + " ---- " + Thread.currentThread().getId());
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic
                                                 characteristic, int status) {
            Log.i("onCharacteristicRead", characteristic.toString());
            gatt.disconnect();
        }

    };

    private String bytesToHex(byte[] bytes) {
        final char[] hexArray = "0123456789ABCDEF".toCharArray();
        char[] hexChars = new char[bytes.length * 2];
        for ( int j = 0; j < bytes.length; j++ ) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }
}
like image 756
abhogu Avatar asked Aug 03 '15 03:08

abhogu


1 Answers

I've had similar problem! I have made some testing and I've determined that the minimum connection interval is 48ms with android api level 18. (connection interval for BLE on Galaxy S3 Android 4.3).

Posible solution:

  1. Make the connection interval slower (less than 48ms).

  2. From api level 21 to upper, you can change CONNECTION_PRIORITY android developer to modify the interval connection but this implies more consumption of energy.

like image 185
Manu Martinez Avatar answered Sep 23 '22 20:09

Manu Martinez