dotintent / FlutterBleLib

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

startPeripheralScan runs only every ODD time #580

Open psycura opened 3 years ago

psycura commented 3 years ago

Hello, and thank you for this library.

I have a strange issue with startPeripheralScan method. First run - runs ok, return results as expected (my device is off, so i don't find him, its ok) Second run - I turn ON my device and run same startPeripheralScan, but nothing happens. Third run - I try to run startPeripheralScan again - and its worked

And for example if i do not turn my device ON, just for simulation - on every EVEN run, startPeripheralScan not works.

My code example:

@override
  Future<void> initClient({@required Function onError}) async {
    Log.debug(tag, "Init devices bloc - _initClient");
    try {
      await _bleManager
          .createClient(
            restoreStateIdentifier: "restore-state-identifier",
            restoreStateAction: (peripherals) async {
              if (peripherals != null) {
                for (var peripheral in peripherals) {
                  Log.info(tag, "\t${peripheral.toString()} ");
                  await peripheral.disconnectOrCancelConnection();
                }
              }
            },
          )
          .catchError((e) => Log.error(tag, "Couldn't create BLE client: $e"));
      await _checkPermissions().catchError(
        (e) async {
          Log.error(tag, e);
          onError(PairingStates.errorNoPermission);
          throw BtIssue();
        },
      );

      await _waitForBluetoothPoweredOn(onError);
    } on BtIsOff {
      onError(PairingStates.errorBtIsOff);

      throw BtIsOff();
    } catch (e) {
      Log.error(tag, 'ERROR CATCHED 3 ${e.toString()}');
      throw BtIssue();
    }
  }
void scanAndPair() async {
    Log.debug(tag, "Ble client created - Start scan for devices");
    var deviceFound = false;
    final deviceName = bleRepo.loadDeviceName();
    await bleService.disconnect();
    await bleService.stopScan();
    await scanSubscription?.cancel();

    final timer = Timer(defaultScanTimeout, () async {
      if (!deviceFound) {
        Log.error(tag, "No Device was found");

        await scanSubscription.cancel();
        await bleService.stopScan();
        pairingState = PairingStates.errorNoDevicesFound;
      }
    });
    Log.debug(
        tag, "Ble client created - Start scan for devices - TIMER STARTED");

    scanSubscription = bleService.scanForDevices().listen((scanResult) async {
      Log.debug(tag, 'Try to find device with name $deviceName');
      Log.info(tag,
          'Found new device name:${scanResult?.advertisementData?.localName} RSSI:${scanResult?.rssi}');

      if (scanResult?.advertisementData?.localName != null &&
          scanResult.advertisementData.localName.trimRight() == deviceName) {
        if (timer.isActive) {
          timer.cancel();
        }

        await bleService.stopScan();

        final bleDevice = BleDevice(scanResult);
        deviceFound = true;
        Log.info(tag,
            'The new device ${scanResult.advertisementData.localName} ${scanResult.peripheral.identifier}');
        bleRepo.saveDeviceId(bleDevice.id);
        device = bleDevice;
        await scanSubscription.cancel();

        connectToDevice(bleDevice: bleDevice, timeout: defaultConnectTimeout);
      }
    });
  }
  Stream<ScanResult> scanForDevices() {
    return _bleManager.startPeripheralScan();
  }

So for every EVEN run app is stuck at Ble client created - Start scan for devices - TIMER STARTED message

dannyalbuquerque commented 3 years ago

Same bug on iOS, I got a XPC connection invalid every "odd" scan

xgrimaldi commented 3 years ago

Same bug. First time, scan is working. I select a device, i go back to list screen and then PeripheralScan is not working. No problem on Android, only with IOS.

JamesMcIntosh commented 3 years ago

@psycura This normally happens if you have called createClient multiple times without destroying it in between. see issues #526 & #532

psycura commented 3 years ago

I understand, but i do not destroy client between the scans. Maybe its because i run same sequence before each scan:

Future<void> initClient({required Function onError}) async {
    Log.debug(tag, "Init devices bloc - _initClient");
    try {
      await _bleManager
          .createClient(
            restoreStateIdentifier: "restore-state-identifier",
            restoreStateAction: (peripherals) async {
              if (peripherals != null) {
                for (final peripheral in peripherals) {
                  Log.info(tag, "\t${peripheral.toString()} ");
                  await peripheral.disconnectOrCancelConnection();
                }
              }
            },
          )
          .catchError((e) => Log.error(tag, "Couldn't create BLE client: $e"));
      await _checkPermissions().catchError(
        (e) async {
          Log.error(tag, e);
          onError(PairingStates.errorNoPermission);
          throw BtIssue();
        },
      );

      await _waitForBluetoothPoweredOn(onError);
    } on BtIsOff {
      onError(PairingStates.errorBtIsOff);

      throw BtIsOff();
    } catch (e) {
      Log.error(tag, 'ERROR CATCHED 3 ${e.toString()}');
      throw BtIssue();
    }
  }

And .createClient cause this issue. If yes, the question is - do i have any option to know if bleManager.client exists?

And also this do not explain why this issue happens only every EVEN scan

JamesMcIntosh commented 3 years ago

@psycura That's what is causing the problem, you are calling createClient multiple times without destroying in between. Either don't create the client multiple times or destroy it before calling create again.

psycura commented 3 years ago

@JamesMcIntosh thanks, i`ll try.

But 3 questions is still open:

  1. Why this happens only in IOS?
  2. Why this happens only every EVEN scan, and every ODD scan works, i do same init sequence every time, and do not destroy client between the scans
  3. Do i have option to check if client exists and initialized?

Thanks

psycura commented 3 years ago

@JamesMcIntosh The solution that you provided, dosn't solve the issue.

I tried 2 ways: destroy and create client before every scan, do not create client if it was created before. The result the same - every EVEN time scan doesn't work

JamesMcIntosh commented 3 years ago

@psycura What if you remove await peripheral.disconnectOrCancelConnection();?

psycura commented 3 years ago

@JamesMcIntosh i`ll try, but this part of code is not executed, because at the initial i dont have any paired peripheral

psycura commented 3 years ago

@JamesMcIntosh As expected - removing of await peripheral.disconnectOrCancelConnection(); does not helps

JamesMcIntosh commented 3 years ago

@psycura You can also make sure that you're not getting any errors on the stream.

BleManager()
      .startPeripheralScan()
      .listen(
        (ScanResult scanResult) { ... },
        onError: (Object e) {
            print("Error listening to scan results: ${e}");
        },
        onDone: () {
            print("Finished listening to scan results");
        },
psycura commented 3 years ago

@JamesMcIntosh No error messages. Straight received Finished listening to scan results message (On EVEN scan)

JamesMcIntosh commented 3 years ago

@psycura If you add some breakpoints into FlutterBleLib/lib/src/bridge/scanning_mixin.dart you might get some more insight. Such as if the _scanEvents field is null when stopDeviceScan is being called.

psycura commented 3 years ago

@JamesMcIntosh And how does it helps me?

JamesMcIntosh commented 3 years ago

@psycura Did it print "Finished listening..." on the first scan too?

I'm just getting you to go through the path I would follow if trying to find the source of the issue since I can't replicate it on my device.

If it's returning again straight away without scanning then you'd be asking if/why the stream is completed. In scanning_mixin you can see if it's reusing the _scanEvents stream and you can check what state it's in for each device scan.

From here it depends how deep you want to go looking... you can follow it into the iOS codebase and see what's happening in the scanningEvents channel and startDeviceScan implementation.

psycura commented 3 years ago

@JamesMcIntosh No at first scan there is no message Finished listening to scan results This is strange because in timer that responsible for TIMOUT i call to stopPeripheralScan

final timer = Timer(defaultScanTimeout, () async {
      if (!deviceFound) {
        Log.error(
            tag, "No Device was found total other devices found: $deviceCount");

        await scanSubscription?.cancel();
        await _bleManager.stopPeripheralScan();
      }
    });

So maybe reason in stopPeripheralScan method, that not worked propertly

JamesMcIntosh commented 3 years ago

@psycura I assume you added the breakpoints I suggested in ScanningMixin to make sure that ScanningMixin.stopDeviceScan() is called when you expect it to be in relation to ScanningMixin.startDeviceScan() as you may be doing something such as subscribing to the same event channel for both attempts then closing it which accessing it the second time.

psycura commented 3 years ago

@JamesMcIntosh, i very appreciate your help, however i can do only changes related to my code, and make sure that i use the library in the proper way. But if issue is happens at the library side, i expect, that it will be fixed by library`s developers. I understand, that we are speaking about open source project, and developer can decide to stop maintain his library. If this is the case - please notify as, and we can decide if and how we will continue to use this library.

Thanks

dannyalbuquerque commented 3 years ago

@psycura For now, I used flutter_blue for the scan on iOS and I continue to use FlutterBleLib by creating a peripheral with the createUnsafePeripheral method.

psycura commented 3 years ago

@dannyalbuquerque you mean use 2 libraries? One for scan, and second for pairing and interaction? Why not to use only flutter_blue?

I created a temporary solution for IOS - check if no devices was found at all, i simple make new scan.

dannyalbuquerque commented 3 years ago

@psycura Yes in case the application is already implemented with FlutterBleLib. It is also a temporary fix.

I created a temporary solution for IOS - check if no devices was found at all, i simple make new scan.

Good idea

JamesMcIntosh commented 3 years ago

@psycura The breakpoints in ScanningMixin are there to identify if your code is correctly interfacing with the iOS implementation. From what I have experienced the iOS part is a lot less forgiving for developer mistakes.

however i can do only changes related to my code, and make sure that i use the library in the proper way.

That's not true, you can manipulate the working copy of the dart code while running Android Studio and even change the iOS code while running the app from XCode.

@psycura @dannyalbuquerque I'd happily run/test an example app which demonstrates the failure if you want to make one up and publish it on your GitHub.

psycura commented 3 years ago

@JamesMcIntosh you can take the example project from the library. The only thing that was changed is in pubspec.yaml added to dependencies flutter_ble_lib: ^2.3.2 First scan - display list of devices Pull to refresh - flutter: 2021-03-18T12:25:15.063781 D DevicesBloc._startScan: Ble client created appears in the log and nothing happens Pull to refresh again - everything works as expected

kamil-chmiel commented 3 years ago

I'm also trying to fix this problem on iOS now and tried what @JamesMcIntosh suggested. In ScanningMixin every other time ScanningMixin.stopDeviceScan() is called right after ScanningMixin.startDeviceScan().

_scanEvents is null every time we call ScanningMixin.stopDeviceScan()

It looks like something deeper (native side) doesn't work properly?

JamesMcIntosh commented 3 years ago

There is a simple workaround this which is to either comment out the _scanSubscription.cancel() or swap it's order with the stopPeripheralScan call in the refresh method.

  Future<void> refresh() async {
    await _bleManager.stopPeripheralScan();
    _scanSubscription.cancel();
    bleDevices.clear();
    _visibleDevicesController.add(bleDevices.sublist(0));
    await _checkPermissions()
        .then((_) => _startScan())
        .catchError((e) => Fimber.d("Couldn't refresh", ex: e));
  }

The source of the problem is due to an extra "close stream" (null) event being queued up on the "Scanning" EventChannel, this event is delivered when you start listening again to the EventChannels broadcast stream in ScanningMixin.startDeviceScan.

streamController.addStream(_scanEvents, cancelOnError: true)

Does anyone have a good idea why it's happening only on iOS, looking at both the java and iOS implementations I would have expected it to occur in Java too?

To see it add a breakpoint inside the EventChannel.receiveBroadcastStream() implementation - platform_channel.dart line 528

  Stream<dynamic> receiveBroadcastStream([ dynamic arguments ]) {
    final MethodChannel methodChannel = MethodChannel(name, codec);
    late StreamController<dynamic> controller;
    controller = StreamController<dynamic>.broadcast(onListen: () async {
      binaryMessenger.setMessageHandler(name, (ByteData? reply) async {
        if (reply == null) { // ADD BREAKPOINT HERE
          controller.close();
        } else {
          try {
JamesMcIntosh commented 3 years ago

@kamil-chmiel @psycura @dannyalbuquerque @mikolak Looks like I have found the bottom of the rabbit hole.

Android succeeds because has protection in the EventChannel#L240 to stop double submission of "end of stream".

By the look of it iOS does not have the same sort of protections https://github.com/flutter/engine/blob/master/shell/platform/darwin/common/framework/Headers/FlutterChannels.h https://github.com/flutter/engine/blob/master/shell/platform/darwin/common/framework/Source/FlutterChannels.mm

The solution may be to track in ScanningStreamHandler whether FlutterEndOfEventStream has been sent to the FlutterEventSink. I've added this to PR #583

This issue probably warrants raising a ticket in the Flutter code base to question the different behaviour.

mikolak commented 3 years ago

Tremendous work, @JamesMcIntosh!

psycura commented 3 years ago

@JamesMcIntosh thanks for help )

xgrimaldi commented 3 years ago

Thanks to all of you for your investigation, extremely appreciated, it will help me a lot in my iOS app ! Especially @JamesMcIntosh .

sakinaboriwala commented 1 year ago

Thankyou @JamesMcIntosh @psycura calling stopPeripheralScan before startPeripherScan worked for me.