Нестабильное BLE-соединение в Android 6 (Зефир)

Я разрабатываю приложение для Android, которое работает с Bluetooth LE. Он хорошо работает на Android 4.3 или 5.0.1 и на разных устройствах, но в Android M (6.0.1) он не стабилен. Я написал облегченный пример проекта, который имеет такое же поведение и проблемы, и ниже весь мой код;

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class MainActivity extends ActionBarActivity {
    private BluetoothAdapter mBluetoothAdapter;
    private int REQUEST_ENABLE_BT = 1;
    private Handler mHandler;
    private static final long SCAN_PERIOD = 2000;
    private BluetoothLeScanner mLEScanner;
    private ScanSettings settings;
    private List<ScanFilter> filters;
    private BluetoothGatt mGatt;

    private boolean connectionState = false, isScanning = false;
    private TextView rssiTxt;
    private RSSITimer rssiTimer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        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();

        // VIEW
        rssiTxt = (TextView) findViewById(R.id.rssiTxt);

        (rssiTimer = new RSSITimer()).startTimer();
    }

    @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) {
                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() {
        rssiTimer.stopTimerTask();

        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(isScanning) {
            return;
        } else {
            isScanning = true;
        }

        if (enable) {
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (Build.VERSION.SDK_INT < 21) {
                        mBluetoothAdapter.stopLeScan(mLeScanCallback);

                        isScanning = false;
                    } else {
                        mLEScanner.stopScan(mScanCallback);

                        isScanning = false;
                    }
                }
            }, 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 = 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();

            if ("PRODi".equals(device.getName()) || "PRODi(TEST)".equals(device.getName())) {
                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());

                            if ("PRODi".equals(device.getName()) || "PRODi(TEST)".equals(device.getName())) {
                                connectToDevice(device);
                            }
                        }
                    });
                }
            };

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

            return;
        } else {
            mGatt.connect();
            scanLeDevice(false);// will stop after first device detection
        }
    }

    private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            switch (newState) {
                case BluetoothProfile.STATE_CONNECTED:

                    Log.i("gattCallback", "STATE_CONNECTED");
                    gatt.discoverServices();
                    connectionState = true;
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(MainActivity.this, "STATE_CONNECTED", Toast.LENGTH_SHORT).show();
                        }
                    });
                    break;
                case BluetoothProfile.STATE_DISCONNECTED:

                    Log.e("gattCallback", "STATE_DISCONNECTED");
                    connectionState = false;
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(MainActivity.this, "STATE_DISCONNECTED", Toast.LENGTH_SHORT).show();
                        }
                    });
                    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());
            gatt.readCharacteristic(services.get(1).getCharacteristics().get(0));
        }

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

        @Override
        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
            super.onReadRemoteRssi(gatt, rssi, status);

            final int rssi2 = rssi;
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    rssiTxt.setText("RSSI: " + rssi2);
                }
            });
        }
    };

    /*TIMER for Read RSSI*/////////////////////////////////////////////////////////////////////////////////////////
    class RSSITimer {
        //we are going to use a handler to be able to run in our TimerTask
        final Handler handler = new Handler();
        private Timer     timer;
        private TimerTask timerTask;

        private int interval = 1000;

        public void startTimer() {
            //set a new Timer
            timer = new Timer();
            //initialize the TimerTask's job
            initializeTimerTask();
            //schedule the timer, after the first 1000ms the TimerTask will run every [INTERVAL]ms
            timer.schedule(timerTask, 1000, interval); //
        }

        public void stopTimerTask() {
            //stop the timer, if it's not already null
            if (timer != null)
            {
                timer.cancel();
                timer = null;
            }
        }

        public void initializeTimerTask() {
            timerTask = new TimerTask() {
                public void run() {
                    handler.post(new Runnable() {
                        public void run() {

                            if (mBluetoothAdapter.isEnabled()) {
                                if (connectionState == false) {
                                    scanLeDevice(true);
                                } else {
                                    mGatt.readRemoteRssi();
                                }
                            }
                        }
                    });
                }
            };
        }
    }
}

У этого есть различные проблемы;

  • не получить обратный вызов readRemoteRssi() через несколько секунд
  • не подключаться снова, после отключения
  • не работает, когда приложение переходит в фоновый режим
  • Bluetooth-чип сбрасывается между отключением и попыткой подключения снова!!
  • ...

я знаю это DeadObjectException произошло в BluetthGatt во время этого вопроса и readRemoteRssi() вернул false, но не может решить это.

Я знаю, что в Android 6 есть некоторые изменения в BLE, и я много искал и изучал документы, но безрезультатно! Но я обнаружил, что когда я установил interval в 100 вместо 1000, работает ЛУЧШЕ, но не Абсолютно (на Android 6.0.1). Да я уверен что interval и как это реализовать (Runnable, AlarmManager, TimerTask, ScheduledExecutorService, ...) эффективны для этого, но ПОЧЕМУ?! Это моя проблема.

Есть идеи? Спасибо и извините за мой английский.

ОБНОВЛЕНИЕ: Вот ссылка на мой журнал, когда я подаю заявку под некоторым давлением. Извините, это больше, чем объем переполнения стека!! Я ясно вижу, что Bluetooth перезапустился в нем...

3 ответа

Включите "Журнал отслеживания Bluetooth HCI" и используйте Wireshark для анализа пакетов. Таким образом, вы можете видеть события стека Bluetooth. Bluetooth HCI Snoop Log Меню

не подключаться снова, после отключения

Выполнить device.connectGatt(getApplicationContext(), true, gattCallback); вместо device.connectGatt(getApplicationContext(), false, gattCallback); для автоматического переподключения.

не работает, когда приложение переходит в фоновый режим

У вас должна быть служба переднего плана или что-то еще в вашем процессе приложения, которое предотвращает уничтожение вашего процесса. Это связано с тем, что API BLE основаны на обратных вызовах, а не на намерениях.

сброс чипа bluetooth между разъединением и попыткой соединиться снова!! не получить обратный вызов readRemoteRssi() через несколько секунд

Определенно ошибка в Android. Подайте отчет об ошибке. Не могли бы вы опубликовать ПОЛНЫЙ вывод logcat (не только вывод, отфильтрованный из вашего приложения)?

не получить обратный вызов readRemoteRssi() через несколько секунд

Понятия не имею об этом...

Похоже, что вы сканируете и подключаете / читаете / пишете одновременно. На Android это приводит к проблемам со стабильностью, и стек bluetooth может дать сбой. Попытайтесь упростить работу со стеком bluetooth, никогда не сканируйте и не подключайте одновременно, подключайтесь только к одному устройству и ждите, пока обратные вызовы не будут возвращены, прежде чем делать следующее, вам даже следует добавить некоторые разрывы между вашими задачами. У нас было много проблем, когда мы слишком много работали со стеком одновременно, например сбойный стек Bluetooth, который больше не мог подключаться к устройствам, пока вы не перезагрузите устройство и так далее.

Другие вопросы по тегам