FlutterBleLib icon indicating copy to clipboard operation
FlutterBleLib copied to clipboard

startPeripheralScan runs only every ODD time

Open psycura opened this issue 3 years ago • 30 comments

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

psycura avatar Mar 09 '21 12:03 psycura

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

dannyalbuquerque avatar Mar 09 '21 21:03 dannyalbuquerque

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.

xgrimaldi avatar Mar 10 '21 16:03 xgrimaldi

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

JamesMcIntosh avatar Mar 16 '21 11:03 JamesMcIntosh

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

psycura avatar Mar 16 '21 12:03 psycura

@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.

JamesMcIntosh avatar Mar 17 '21 04:03 JamesMcIntosh

@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 avatar Mar 17 '21 07:03 psycura

@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

psycura avatar Mar 17 '21 07:03 psycura

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

JamesMcIntosh avatar Mar 17 '21 10:03 JamesMcIntosh

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

psycura avatar Mar 17 '21 10:03 psycura

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

psycura avatar Mar 17 '21 10:03 psycura

@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");
        },

JamesMcIntosh avatar Mar 17 '21 10:03 JamesMcIntosh

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

psycura avatar Mar 17 '21 10:03 psycura

@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.

JamesMcIntosh avatar Mar 17 '21 10:03 JamesMcIntosh

@JamesMcIntosh And how does it helps me?

psycura avatar Mar 17 '21 10:03 psycura

@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.

JamesMcIntosh avatar Mar 17 '21 11:03 JamesMcIntosh

@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

psycura avatar Mar 17 '21 12:03 psycura

@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.

JamesMcIntosh avatar Mar 18 '21 00:03 JamesMcIntosh

@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

psycura avatar Mar 18 '21 08:03 psycura

@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.

dannyalbuquerque avatar Mar 18 '21 08:03 dannyalbuquerque

@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.

psycura avatar Mar 18 '21 08:03 psycura

@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

dannyalbuquerque avatar Mar 18 '21 08:03 dannyalbuquerque

@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.

JamesMcIntosh avatar Mar 18 '21 10:03 JamesMcIntosh

@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

psycura avatar Mar 18 '21 10:03 psycura

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?

kamil-chmiel avatar Mar 18 '21 12:03 kamil-chmiel

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 avatar Mar 19 '21 00:03 JamesMcIntosh

@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.

JamesMcIntosh avatar Mar 19 '21 12:03 JamesMcIntosh

Tremendous work, @JamesMcIntosh!

mikolak avatar Mar 19 '21 16:03 mikolak

@JamesMcIntosh thanks for help )

psycura avatar Mar 21 '21 08:03 psycura

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

xgrimaldi avatar Mar 26 '21 14:03 xgrimaldi

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

sakinaboriwala avatar Feb 09 '23 17:02 sakinaboriwala