Нестабильное 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, который больше не мог подключаться к устройствам, пока вы не перезагрузите устройство и так далее.