0

BluetoothManager is a class responsible for interaction with a ble device.
It is injected as a singelton with RxBleClient and starts scanning on fragment/service's start method. Sometimes scanning is stopped automatically and sometimes after few seconds and I can't find the reason of it.

The thing is that the device is still broadcasting its packages, but my application is not able to receive it as BluetoothAdapter has already stopped the scan.

Here are logs from scanning in a service:

03-09 11:17:58.526 10196-10196/com.turboegg.storm D/BluetoothManager  ***   212: Clear scan
03-09 11:17:58.527 10196-10196/com.turboegg.storm D/BluetoothManager  ***   72: Start scanning
03-09 11:17:58.579 10196-10196/com.turboegg.storm I/AppCompatViewInflater: app:theme is now deprecated. Please move to using android:theme instead.
03-09 11:17:58.589 10196-10196/com.turboegg.storm I/AppCompatViewInflater: app:theme is now deprecated. Please move to using android:theme instead.
03-09 11:17:58.602 10196-10196/com.turboegg.storm I/AppCompatViewInflater: app:theme is now deprecated. Please move to using android:theme instead.
03-09 11:17:58.604 10196-10196/com.turboegg.storm I/AppCompatViewInflater: app:theme is now deprecated. Please move to using android:theme instead.
03-09 11:18:01.592 10196-10196/com.turboegg.storm D/BluetoothAdapter: startLeScan(): null
03-09 11:18:01.596 10196-10196/com.turboegg.storm D/BluetoothAdapter: STATE_ON
03-09 11:18:01.600 10196-10207/com.turboegg.storm D/BluetoothLeScanner: onClientRegistered() - status=0 clientIf=8
03-09 11:18:01.656 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device C6:86:B6:35:FF:00
03-09 11:18:01.784 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device EE:AC:C7:32:9A:F6
03-09 11:18:02.643 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device C6:86:B6:35:FF:00
03-09 11:18:02.835 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device EE:AC:C7:32:9A:F6
03-09 11:18:03.659 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device C6:86:B6:35:FF:00
03-09 11:18:03.796 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device EE:AC:C7:32:9A:F6
03-09 11:18:04.663 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device C6:86:B6:35:FF:00
03-09 11:18:04.785 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device EE:AC:C7:32:9A:F6
03-09 11:18:05.648 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device C6:86:B6:35:FF:00
03-09 11:18:05.809 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device EE:AC:C7:32:9A:F6
03-09 11:18:06.817 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device EE:AC:C7:32:9A:F6
03-09 11:18:07.672 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device C6:86:B6:35:FF:00
03-09 11:18:07.827 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device EE:AC:C7:32:9A:F6
03-09 11:18:08.682 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device C6:86:B6:35:FF:00
03-09 11:18:08.826 10196-10196/com.turboegg.storm D/BluetoothManager  ***   79: Device EE:AC:C7:32:9A:F6
03-09 11:18:09.009 10196-10196/com.turboegg.storm D/BluetoothAdapter: stopLeScan()
03-09 11:18:09.011 10196-10196/com.turboegg.storm D/BluetoothAdapter: STATE_ON

Here is BluetoohManager:

public class BluetoothManager {

    private Context context;
    private RxBleClient rxBleClient;
    private StorageManager storageManager;
    private Subscription scanSubscription, connectionSubscription, notificationSubscription, writeSubscription, keepAwakenSubscription;
    private RxBleDevice connectedDevice;
    private RxBleConnection activeConnection;

    public BluetoothManager(Context context, RxBleClient rxBleClient, StorageManager storageManager) {
        this.context = context;
        this.rxBleClient = rxBleClient;
        this.storageManager = storageManager;
    }

    public void onDestroy() {
        stop();
        storageManager.setAllSensorsDisconnectible();
        storageManager.clearResources();
    }

    public void stop() {
        clearScan();
        disconnect();
    }

    public void stopScan() {
        clearScan();
    }

    public void disconnect() {
        clearConnection();
        clearNotification();
        clearWrite();
        clearKeepAwaken();
    }

    public void startScan() {
        clearScan();
        Timber.d("Start scanning");
        scanSubscription = Observable.timer(3, TimeUnit.SECONDS)
                .flatMap(ignored -> rxBleClient.scanBleDevices())
                .filter(rxBleScanResult -> CommonUtils.isStormSensor(rxBleScanResult.getBleDevice()))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(rxBleScanResult -> {
                    RxBleDevice device = rxBleScanResult.getBleDevice();
                    Timber.d("Device %s", device.getMacAddress());
                    if (device.getMacAddress() != null && !storageManager.containsDiscoveredDevice(device.getMacAddress())) {
                        Timber.d("Found device, %s %s", device.getName(), device.getMacAddress());
                        storageManager.addDiscoveredSensor(device.getMacAddress(), device.getName());
                        EventBus.getDefault().post(new DeviceDiscovered(device.getMacAddress(), device.getName()));
                    }
                    storageManager.setSensorConnectible(device.getMacAddress(), Calendar.getInstance().getTime());
                }, this::onBleFailure);
    }

    public void connectDevice(String macAddress, String outInDoor) {
        disconnect();
        connectedDevice = rxBleClient.getBleDevice(macAddress);
        if (connectedDevice != null) {
            Timber.d("Start connecting to %s", macAddress);
            connectionSubscription = connectedDevice.establishConnection(context, false)
                    .subscribe(connection -> {
                        // Need to delay write operations due to sensor processor limitations
                        writeCharacteristic(connection, CommonUtils.getBleTime())
                                .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
                        writeCharacteristic(connection, AppConstants.BLUETOOTH_SET_BUZZER_OFF)
                                .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
                        writeCharacteristic(connection, AppConstants.BLUETOOTH_SET_MASK)
                                .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
                        writeCharacteristic(connection, AppConstants.BLUETOOTH_CLEAR_STATS)
                                .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
                        writeCharacteristic(connection, AppConstants.BLUETOOTH_CLEAR_MEM)
                                .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
                        writeCharacteristic(connection, outInDoor)
                                .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
                        writeCharacteristic(connection, AppConstants.BLUETOOTH_CLEAR_MASK)
                                .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
                        writeCharacteristic(connection, AppConstants.BLUETOOTH_SET_MASK)
                                .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
                        activeConnection = connection;
                        getBasicParams(connection, macAddress);
                        subscribeForNotifications(connection, macAddress);
                    }, throwable -> {
                        Timber.e("Error while connecting " + throwable.toString());
                        handleConnectionError(macAddress);
                        disconnect();
                    });
        }
    }

    public void writeOutInDoor(String outInDoor) {
        clearWrite();
        writeSubscription = Observable.combineLatest(
                writeCharacteristic(activeConnection, AppConstants.BLUETOOTH_CLEAR_STATS)
                        .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
                writeCharacteristic(activeConnection, AppConstants.BLUETOOTH_CLEAR_MEM)
                        .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
                writeCharacteristic(activeConnection, outInDoor)
                        .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
                writeCharacteristic(activeConnection, AppConstants.BLUETOOTH_CLEAR_MASK)
                        .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
                writeCharacteristic(activeConnection, AppConstants.BLUETOOTH_SET_MASK)
                        .delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
                (bytes, bytes2, value, bytes4, bytes5) -> value)
                .subscribe(value -> Timber.d("Written value %s", new String(value)),
                        throwable -> Timber.e("Error writing outInDoor: %s", throwable.toString())
                );
    }

    public void handleDeviceStatus(boolean checked) {
        if (checked) {
            keepDeviceAwaken();
        } else {
            clearKeepAwaken();
        }
    }

    public void handleDeviceDisconnection(String macAddress) {
        if (connectedDevice != null && connectedDevice.getMacAddress().equals(macAddress)) {
            disconnect();
        }
    }

    /**
     * Private methods
     */

    private void getBasicParams(RxBleConnection connection, String macAddress) {
        Observable.combineLatest(
                connection.readCharacteristic(AppConstants.BLUETOOTH_UUID_BATTERY),
                connection.readCharacteristic(AppConstants.BLUETOOTH_UUID_FIRMWARE),
                (bytes, bytes2) -> new DeviceConnected(bytes, bytes2, macAddress))
                .take(1)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(deviceConnected -> {
                    Timber.d("Bat %s", deviceConnected.getBattery());
                    Timber.d("Fmw %s", deviceConnected.getFirmware());
                    storageManager.updateSensorInfo(macAddress, deviceConnected.getBattery(), deviceConnected.getFirmware());
                    storageManager.setSensorConnected(macAddress);
                    handleDeviceStatus(storageManager.getStormSensor(macAddress).isStatusChecked());
                    EventBus.getDefault().post(deviceConnected);
                }, throwable -> Timber.e("Sensor basic info error %s", throwable.getMessage()));

    }

    private void subscribeForNotifications(RxBleConnection connection, String macAddress) {
        notificationSubscription = Observable.combineLatest(
                connection.setupNotification(AppConstants.BLUETOOTH_UUID_EVENT)
                        .<byte[]>flatMap(observable -> observable),
                connection.setupNotification(AppConstants.BLUETOOTH_UUID_DISTANCE)
                        .<byte[]>flatMap(observable -> observable),
                NotificationStormEvent::new)
                .filter(CommonUtils::isStormEvent)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(notificationStormEvent -> {
                    Timber.d("New storm, event: %s, distance %s km", notificationStormEvent.getEvent(), notificationStormEvent.getDistance());
                    storageManager.addStormEvent(Calendar.getInstance().getTime(), notificationStormEvent.getDistance());
                    EventBus.getDefault().post(notificationStormEvent);
                }, throwable -> Timber.e("Notification error %s", throwable.toString()));
    }

    private void keepDeviceAwaken() {
        clearKeepAwaken();
        keepAwakenSubscription = Observable.interval(
                AppConstants.BLUETOOTH_KEEP_AWAKE_MINS, AppConstants.BLUETOOTH_KEEP_AWAKE_MINS, TimeUnit.MINUTES)
                .flatMap(aLong -> writeCharacteristic(activeConnection, CommonUtils.getBleTime()))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(bytes -> Timber.d("Written value %s to keep device awaken", new String(bytes)),
                        throwable -> Timber.e("Error keeping awake %s", throwable.toString()));
    }

    private void onBleFailure(Throwable throwable) {
        if (throwable instanceof BleScanException) {
            handleBleScanException((BleScanException) throwable);
        }
    }

    private void clearScan() {
        Timber.d("Clear scan");
        if (scanSubscription != null) {
            scanSubscription.unsubscribe();
            Timber.d("Clear unsubscribed");
        }
        scanSubscription = null;
    }

    private void clearConnection() {
        if (connectionSubscription != null) connectionSubscription.unsubscribe();
        connectionSubscription = null;
        connectedDevice = null;
        activeConnection = null;
    }

    private void clearNotification() {
        if (notificationSubscription != null) notificationSubscription.unsubscribe();
        notificationSubscription = null;
    }

    private void clearWrite() {
        if (writeSubscription != null) writeSubscription.unsubscribe();
        writeSubscription = null;
    }

    private void clearKeepAwaken() {
        if (keepAwakenSubscription != null) keepAwakenSubscription.unsubscribe();
        keepAwakenSubscription = null;
    }

    private Observable<byte[]> writeCharacteristic(RxBleConnection connection, String value) {
        byte[] bytesToWrite = value.getBytes(StandardCharsets.UTF_8);
        return connection.writeCharacteristic(AppConstants.BLUETOOTH_UUID_UART_WRITE, bytesToWrite);
    }

    private void handleBleScanException(BleScanException bleScanException) {
        switch (bleScanException.getReason()) {
            case BleScanException.BLUETOOTH_NOT_AVAILABLE:
                Timber.e("Bluetooth is not available");
                break;
            case BleScanException.BLUETOOTH_DISABLED:
                Timber.e("Enable bluetooth and try again");
                break;
            case BleScanException.LOCATION_PERMISSION_MISSING:
                Timber.e("On Android 6.0 location permission is required. Implement Runtime Permissions ");
                break;
            case BleScanException.LOCATION_SERVICES_DISABLED:
                Timber.e("Location services needs to be enabled on Android 6.0");
                break;
            case BleScanException.BLUETOOTH_CANNOT_START:
            default:
                Timber.e("Unable to onStart scanning");
                break;
        }
    }

    private void handleConnectionError(String macAddress) {
        // This is the case where e.g. connection was lost or the device was turned off
        // Need to call for realm to modify the sensor object in different thread
        Realm realm = Realm.getDefaultInstance();
        realm.executeTransaction(realm1 -> {
            StormSensor sensor = realm1.where(StormSensor.class).equalTo(AppConstants.STORM_SENSOR_MAC_ADDRESS, macAddress).findFirst();
            sensor.setConnected(false);
        });
        realm.close();
    }
}
  • Have you tried to put a log in the `clearScan()` method? – Dariusz Seweryn Mar 08 '17 at 11:45
  • I've just added two logs at the begining and inside if condition in this method. It proves that `scanSubscription` is `null` at start and `stopLeScan()` is not invoked by the `clearScan()` method. – matysiatko Mar 08 '17 at 12:49
  • Could you update the original post code and logs to match the current state? – Dariusz Seweryn Mar 08 '17 at 12:58
  • Yes, please. Done. – matysiatko Mar 08 '17 at 13:14
  • You might have found a race condition in the `RxAndroidBle` library - what if you would not call `clearScan()` in `startScan()` but some time before? – Dariusz Seweryn Mar 08 '17 at 13:51
  • Hoped that was the case, but I've delayed subscription for 5 sec and added a log for each `rxBleScanResut` received and it seems it didn't make any difference. – matysiatko Mar 08 '17 at 14:23
  • Change the start of subscription to: `Observable.timer(5, TimeUnit.SECONDS).flatMap { ignored -> rxBleClient.startScan() }` instead of `rxBleClient.scanBleDevices().delaySubscription(5, TimeUnit.SECONDS)` – Dariusz Seweryn Mar 08 '17 at 15:32
  • It didn't make any difference. Strage as it may seem yesterday evening scanning worked well and stably, but today morning it stops unexpectedly again. I look for any reason of that strage behavior and have no clue what it might be. In manifest, there are `ACCESS_FINE_LOCATION`, `BLUETOOTH`, `BLUETOOTH_ADMIN` and `INTERNET` permissions as well as uses-features `location.network` and `location.gps` as my device runs on Android M. Permissions and is bluetooth enabled are checked at runtime. – matysiatko Mar 09 '17 at 10:07
  • 1
    There is another possible source of the problem. The `RxAndroidBle` is using a legacy scanning interface which is translated to the `BluetoothLeScanner` by the OS. The new API is starting and stopping the scan for periods of time depending on the settings. Maybe there is a bug in this translation - as I do not see any place in your code that would stop the scanning. – Dariusz Seweryn Mar 09 '17 at 10:28
  • Thank you for your help. As for now, in a service that uses `BluetoothManager` I have created a timer that periodically re-starts the scanning but it doesn't resolve the problem, as sometimes `BluetoothAdapter` works for couple of seconds and, the other time, it stops right after the scan's start. – matysiatko Mar 09 '17 at 11:53
  • You can also try the raw Android BLE API for scanning part and after the scan start using the `RxBleLibrary`. I would like to know what is the core issue here but it looks like the OS is stopping the scan for whatever reason. – Dariusz Seweryn Mar 09 '17 at 12:04

0 Answers0