dotintent / FlutterBleLib

Bluetooth Low Energy library for Flutter with support for simulating peripherals
Apache License 2.0
535 stars 197 forks source link

Incorrect Bluetooth adapter state on iOS #532

Open harriakash opened 3 years ago

harriakash commented 3 years ago

Device bluetooth status returns "bluetoothstate.UNKNOWN" while checking more than once in ios 13. Working fine in android and < ios 13

Same issue while scanning devices or connecting devices for more than once.

It works the first time, thereafter it doesn't work. Had to restart the app to get it working in ios13.

flukejones commented 3 years ago

I'm currently battling this. It seems to appear after a period of time so I'm wondering if there is another status type for iOS, such as for powersaving?

The status will change to POWERED_ON after turning BLE off then on via iOS.

While BLE lib is returning UNKNOWN, other apps using BLE on iOS work fine.

Additionally I am currently treating it as if it is something like "POWERSAVE_ON", so the hardware is on and enabled but in low-power mode. It looks to be working fine. I only treat it as this if iOS + bluetoothstate.UNKNOWN.

mikolak commented 3 years ago

Will calling bleManager.bluetoothState() twice in a row reproduce the issue? I don't have iOS 13 on my hands, but I'll try to get one.

flukejones commented 3 years ago

I have two observeBluetoothState listeners running. Unfortunately it's a little hard to determine exactly how to reproduce this since it is either random or time related.

Running my app just now works fine with the above and the UNKNOWN handling code removed. The device reports POWERED_ON. I'll keep trying to narrow it down over the course of my day.

flukejones commented 3 years ago

My iOS version is 12.4.8 btw. I test using an older device.

flukejones commented 3 years ago

One of my colleagues at work mentions this:

Basically the problem where on windows restore

override func viewDidAppear(_ animated: Bool) {
//start the scan
lst_aura.reloadData()
saved_auras=Preferences.getAuras()
aura_devices=[]
startScan()
}
override func viewDidDisappear(_ animated: Bool) {
scan_disposable?.dispose()
}
private func startScan()
{
//initialize Central Manager
state = ble_manager!.state
scan_disposable?.dispose()
scan_disposable=ble_manager!.observeState()
.startWith(state!)
.filter { $0 == .poweredOn }
.flatMap { _ in self.ble_manager!.scanForPeripherals(withServices: nil)
}

state was initialized at the app start but for reason i didn't fully understand must be regain from the central manager every time a scan is started otherwise it lead to status UNKNOWN

flukejones commented 3 years ago

The key difference I think is this occurs more in the release build. In fact it seems quite consistently to occur in release builds...

Right so, I tracked it down. Debug async seems slower than release, right? It turns out I wasn't awaiting on a function call that sets up the createClient().

So a startPeripheralScan() was trying to run at the same time or before createClient(), which in release mode was running much faster, and occasionally beating the setup call to the punch.

mikolak commented 3 years ago

@flukejones I assume that means it's fixed on your side?

@harriakash do you have any specific reproduction steps?

harriakash commented 3 years ago

@mikolak

Please, look into this code snippet,

`import 'package:flutter_ble_lib/flutter_ble_lib.dart';

BleManager bleManager = BleManager(); await bleManager.createClient();

testBLE() async { await bleManager.createClient(); BluetoothState currentState = await bleManager.bluetoothState(); print("Ios BLE State debugging" +" " + currentState.toString()); } `

Call testBLE() on button click,

First time it says, "bluetoothstate.connected"

When clicked again, there after it says, "bluetoothstate.UNKNOWN"

Only in ios 13 and later

PFA, main.dart

main.zip

albo1337 commented 3 years ago

I have the same error.

albo1337 commented 3 years ago

@harriakash maybe I have found some kind of "workaround" which can help you:

Future _waitForBluetoothPoweredOn() async {
    const timeout = const Duration(seconds: 3);
    Completer completer = Completer();
    StreamSubscription<BluetoothState> subscription;
    Timer(timeout, () async {
      if (!completer.isCompleted) {
        await subscription.cancel();
        completer.completeError(Future.error(SomeError()));
      }
    });
    subscription =
        _bleManager.observeBluetoothState(emitCurrentValue: true).listen(
      (bluetoothState) async {
        print(bluetoothState);
        if (bluetoothState == BluetoothState.POWERED_ON &&
            !completer.isCompleted) {
          await subscription.cancel();
          completer.complete(true);
        }
      },
    );
    return completer.future;
  }

await this method and normaly you should get bluetoothstate.connected if you do then:

BluetoothState currentState = await bleManager.bluetoothState();
print("Ios BLE State debugging" +" " + currentState.toString());

Could you test this aswell?

mikolak commented 3 years ago

@harriakash You cannot call createClient() multiple times without destroying the old one (destroyClient()). You also have to always make sure that is exists, ie. wait for createClient() to finish before doing any operations. I'm on iOS 14.2 and tested with button calling this:

  Future<BluetoothState> bluetoothState() async {
//    await _bleManager.createClient();
    return _bleManager.bluetoothState();
  }

If createClient() is uncommented I am stuck on UNSUPPORTED, but if that's commented out it works fine no matter how many times I press it.

flukejones commented 3 years ago

@mikolak it looks like I've fixed all my issues. Even though the API is async, some operations do require a strict sync order.

albo1337 commented 3 years ago

@flukejones , could you share your code snippets of how you achieved your goal to fix all your issues?

flukejones commented 3 years ago

@albo1337 I can't sorry. But the general order of things is wrapped in a service in a bloc.

createClient must be guaranteed to run and complete before any other calls.

On connect to device the scan is stopped, then on disconnect it is started again.

All of this is wrapped in streams and listeners tightly integrated in the app bloc flows.

KrystofM commented 3 years ago

Experiencing the same problem. It seems the problem popups when calling for the state after creating the client, even when the client is being awaited. But sometimes it works just okay so I only came across on it after multiple uses.

@mikolak I tried calling bleManager.bluetoothState() twice in a row and that seems to work without a problem.

mikolak commented 3 years ago

Perhaps the promise returns too fast? I'll recheck this, but I'll be thankful for a stable reproduction path.

mikolak commented 3 years ago

@harriakash @albo1337 @KrystofM what iPhones are you testing on?

KrystofM commented 3 years ago

@mikolak iPhone 7 Plus iOS 14.1

mikolak commented 3 years ago

@KrystofM can you make sure createClient() is called only once? Can you check if it also occurs in the release mode?

KrystofM commented 3 years ago

@mikolak Yeah I am checking for if the client has been created already whenever I call it.

Future<void> startBle() async {
    if (_isClientCreated) {
      return;
    }

    await createClient();

    _isClientCreated = true;
  }

So the only way it could be called multiple times is if the client is not destroyed when doing a Hot Restart in debug. I am not so sure about the release mode though as the issue only comes up sometimes and have not found a trigger that could cause it.

mikolak commented 3 years ago

Unless you explicitly destroy the client on reload, it is not destroyed. Client is only destroyed when the whole native application is killed or when destroyClient() was called.

KrystofM commented 3 years ago

@mikolak Are you talking about Hot Reload or Hot Restart?

mikolak commented 3 years ago

Well, neither does anything with the native code IIRC, they only swap the Dart VM or its state, so they're not clearing native state. This means that doing a hot restart might create a second client, which might lead to a leak and unexpected behaviour.

I'm thinking about adding an API to query the state of the client, ie. whether it exists, but I can't give any time line for it.

KrystofM commented 3 years ago

Anyway is there a simpler way of knowing whether the client has been created? Something like bleManager.isClientCreated() would be nice or implementing it straight to the createClient() also

Oh yeah commented at the same time. Yeah that feature would be nice!

KrystofM commented 3 years ago

@mikolak Yeah actually noticed there was a leakage a couple of times while developing, so checking the client state would be very helpful as I can’t really see a better way of keeping a track of the state outside.

bot509 commented 3 years ago

I encounter this problem on iOS 14 release mode, the only way that I found is wait for state change to power on.

await bleManager.observeBluetoothState().firstWhere((element) => element == BluetoothState.POWERED_ON);
bleManager.startPeripheralScan( ).listen((btState) {});
JamesMcIntosh commented 3 years ago

@mikolak Do you know why the client isn't treated as a singleton or an error throw if you try call createClient a second time without destroying it? Either of those should help stop this error as the behaviour a little bit unexpected and hard to debug.

mikolak commented 3 years ago

The error isn't thrown to allow for swapping clients (turning on simulation with blemulator) and, probably mostly, an oversight. But yeah, there should be a warning, a getter for the client state and perhaps a flag allowing you to destroy the old client when recreating a new one.

mikolak commented 3 years ago

There's a new API for checking whether the native client exists (#589), but it won't get released until I sort out type safety issues (#591). If necessary you can temporarily use a git dependency (commit hash aaa2009).

ahoelzemann commented 3 years ago

I'm running into the same problem on iOS 14 and an iPhone X. It happens exactly on the third try to connect to my BLE device.