Нестабильное соединение BLE в Android 6 (Marshmallow)

Я разрабатываю приложение для 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, ...) эффективны для этого, но ПОЧЕМУ?! Это мой вопрос.

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

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


person YUSMLE    schedule 18.07.2016    source источник
comment
Отправьте отчет об ошибке в систему отслеживания ошибок Android.   -  person Emil    schedule 18.07.2016
comment
Что это значит? Я имею в виду, мой код и подход чисты? Вы проверяли это или столкнулись с этой проблемой ?!   -  person YUSMLE    schedule 19.07.2016


Ответы (3)


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

person HamidReza    schedule 27.07.2016

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

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

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

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

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

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

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

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

person Emil    schedule 19.07.2016
comment
Во-первых, спасибо за внимание. Я пробовал device.connectGatt(getApplicationContext(), true, gattCallback) раньше, но безрезультатно! А что касается don't work when application goes to background, я имею в виду нажатие кнопки «Домой» и касание приложения без промедления и без каких-либо других приложений, которые потребляют память. Мой оригинальный проект использует службы для фоновой работы. В любом случае, что вы думаете о Shutting down VM в logcat? Также я обновил свой logcat. - person YUSMLE; 19.07.2016
comment
Выключение виртуальной машины происходит потому, что ваше приложение выдает необработанное исключение. Вы не должны использовать device.getName().equals(PRODi) в обработчике сканирования — getName() может вернуть null, если рекламные данные не содержат никакого имени. Вместо этого используйте PRODi.equals(device.getName()), так как это никогда не вызывает исключения. - person Emil; 19.07.2016
comment
Вы правы в этом случае, но это не корень моих проблем (на Android 6). выложу новые логи... - person YUSMLE; 19.07.2016

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

person p2pkit    schedule 21.07.2016
comment
Я снова прочитал rssi в обратном вызове onReadRemoteRssi() (с postDelay), но никакого эффекта! Когда убираю чтение rssi совсем, все ОК! - person YUSMLE; 23.07.2016
comment
Вы можете использовать сканирование для получения значений RSSI, если это поможет. - person p2pkit; 25.07.2016
comment
Для подключенного устройства onLecan() или onScanResult() не запускаются. - person YUSMLE; 25.07.2016
comment
Я ничего не смог найти в журналах, но, возможно, чтение RSSI не удается, потому что устройство отключено. Иногда устройство отключается через некоторое время. Вы должны остановить RssiTimer после отключения. - person p2pkit; 25.07.2016
comment
Так почему же я не получаю обратный вызов onConnectionStateChange?! - person YUSMLE; 25.07.2016
comment
Вероятно, потому что стек Bluetooth перезапустился. У вас есть несколько устройств с Android 6? Если да, то поведение на всех телефонах одинаковое? Если у вас нет возможности протестировать на другом устройстве Marshmallow? Может проблема в конкретном устройстве. - person p2pkit; 25.07.2016
comment
У меня есть два устройства с Marshmallow и одинаковое поведение (не совсем так, но у обоих одинаковые проблемы). У меня Galaxy S6 с официальным обновлением для Android 6.0.1 и Galaxy S4 с Cyanogen Android 6.0.1! - person YUSMLE; 26.07.2016
comment
Сейчас я протестировал Sony Z5 и Samsung S6 с Android 6.0.1, я удалил вызов gatt.disconnect в обратном вызове onCharacteristicRead(), и он работает нормально. Я использовал другой телефон Android в качестве периферийного устройства. Должен ли я ждать в течение более длительного времени, пока это не произойдет? - person p2pkit; 26.07.2016
comment
Я его удалил, мне он был не нужен, и ничего не изменилось. Я думаю, вы должны ждать дольше. Были ли у вас мои проблемы на ваших телефонах до этого изменения? В любом случае спасибо за внимание! - person YUSMLE; 27.07.2016
comment
Я попробую еще раз завтра, так как сейчас я тороплюсь. Я попробовал ваш пример без изменений, и он также работал, он просто отключался и снова подключался между чтением rssi. Вы подключаетесь к другому Android-смартфону или к чему-то еще? - person p2pkit; 27.07.2016
comment
Нет, просто подключитесь к одному ключу ble. - person YUSMLE; 28.07.2016
comment
Возможно, проблема в вашем ключе ble, у меня сейчас ваше приложение работает 1,5 часа, и оно все еще читает rssi. - person p2pkit; 28.07.2016