PhilipsHue / flutter_reactive_ble

Flutter library that handles BLE operations for multiple devices.
https://developers.meethue.com/
Other
663 stars 326 forks source link

iOS: background scan #568

Closed msd05keisuke closed 1 year ago

msd05keisuke commented 2 years ago

Hello.

I would like to do a BLE scan on an iOS device in background state.

But I can't do it at the moment. Is there any solution?

I am assuming the following case.

1, iBeacon region in 2, state suspend -> background (10sec) 3, connect to sensor during 10sec and upload sensed information to cloud

By doing this, I would like to achieve sensor-driven sensing, not user-driven.

info.plist

<key>UIBackgroundModes</key>
    <array>
        <string>bluetooth-central</string>
    </array>
akospwc commented 2 years ago

This should work without introducing iBeacons: your app initiates scanning for nearby devices using FlutterReactiveBle.scanForDevices(), and iOS automatically resumes your app to deliver the related events. After this, if your app connects to the discovered nearby devices, it will be able to receive incoming BLE packets. No 10-second timeout applies here.

Now, this only works until your user decides to swipe-kill the iOS app. When that happens, your app won't receive any BLE events until the user manually restarts the app. This is where iBeacons become useful, as iBeacon location events are delivered to terminated apps as well, and iOS will restart your app after swipe-kills or even after the phone is rebooted. iBeacon functionality is not included in this library though, you'll need a separate package for that. We are using flutter_beacon for this.

msd05keisuke commented 2 years ago

@akospwc

I'm coding like this, but I can't scan in the background. Can you tell me the solution? I'm not good at English, so I'm sorry if there is a part that I can't communicate well.

// monitoring(use flutter_beacon)
    StreamSubscription streamMonitoring =
        flutterBeacon.monitoring(regions).listen((result) async {
      if (result.region.proximityUUID!.toLowerCase() ==
              BEACON_UUID.toLowerCase() &&
          result.monitoringState == MonitoringState.inside) {
        debugPrint("IN");

    // scan ble(use flutter_reactive_ble)
        flutterReactiveBle.scanForDevices(withServices: []).listen((device) {
        //code for handling results
        print(device.id + device.name);
        });

      } else if (result.monitoringState == MonitoringState.outside ||
          result.monitoringState == MonitoringState.unknown) {
        debugPrint("OUT");
      }
    });
akospwc commented 2 years ago

Your English is perfectly understandable, don't worry about that even for a second.

Which part of the code is never getting called? Is the problem that the beacon is not found, or that the BLE scan is not working? How are you testing the background execution?

As I said earlier, I'd leave the beacon part out of the equation for now, as it is only needed if your app is explicitly killed by the user. Solve that problem later. As the first step, I'd recommend focusing on the 'happy path', e.g.

Test that the scanning finds your device when the app is running in the foreground. Then send the app to the background (do not swipe kill it, just return to the home screen) and test the scanning again (I recommend sending local notifications to debug these flows - we're using the flutter_local_notifications package for this).

If these flows work, then you can introduce iBeacons to the mix. Apologies if I'm stating the obvious, but iBeacons are completely different animals from regular BLE services, so your communication partner (the sensor) has to advertise itself as iBeacon as well as a regular BLE service for this to work.

Taym95 commented 1 year ago

I will close this issue, because it appears not related to library

HugoHeneault commented 11 months ago

Hello @Taym95 @akospwc

I'm also encoutering this kind of issue.

Here is my dart provider:


class BleScanDevicesNotifier extends StateNotifier<List<DiscoveredDevice>> {
  final Ref ref;
  StreamSubscription<DiscoveredDevice>? subscription;

  BleScanDevicesNotifier(this.ref) : super([]) {
    Timer.periodic(const Duration(seconds: 1), (timer) {
      debugPrint('tick ${timer.tick.toString()}');
    });

    init();
  }

  init() {
    print('init');

    subscription?.cancel();
    subscription =
        ref.read(bleProvider).scanForDevices(withServices: []).listen((device) {
      final knownDeviceIndex = state.indexWhere((d) => d.id == device.id);

      print('received device ${device.id}');

      if (knownDeviceIndex >= 0) {
        state[knownDeviceIndex] = device;
      } else {
        state.add(device);
      }

      state = [...state];
    }, onError: (err) {
      print(err);
      throw err;
    });
  }
}

final bleScanDevicesProvider =
    StateNotifierProvider<BleScanDevicesNotifier, List<DiscoveredDevice>>(
        (ref) => BleScanDevicesNotifier(ref));

When I start the app I have many received device ... logs and tick 1, 'tick 2`... logs

As soon as I switch to another app, without killing it, received device... logs stop, but I still have tick 10, tick 11 coming (to ensure my provider keeps running in background)

I also tried to detect when app goes in background a call init() again; no luck.

As soon as the app comes back to foreground, received device... logs come back again.

Here is my UIBackgroundModes:

<key>UIBackgroundModes</key>
        <array>
            <string>fetch</string>
            <string>location</string>
            <string>bluetooth-central</string>
            <string>bluetooth-peripheral</string>
        </array>

Any ideas?

Thanks for your help :)

HugoHeneault commented 11 months ago

I also reproduce it simply on the example project :

  1. Start scanning
  2. Enable verbose logging and check if XDebug console logs correctly
  3. Send app to background : logs stops
  4. As soon as app comes back to foreground, logs start again

Thanks for your help :)

HugoHeneault commented 11 months ago

@msd05keisuke Did you find a way to get it working since 2022 ? 😇

expoders8 commented 4 months ago

Hello.

do not working Background mode in BLE For flutter_reactive_ble 5.3.1 IOS flutter

But I can't do it at the moment. Is there any solution?

i am using permission

UIBackgroundModes blutooth-central fetch location proceessing scan code : Completer completer = Completer(); late StreamSubscription scanSubscription; scanSubscription = instance .scanForDevices( requireLocationServicesEnabled: false, withServices: [], //withServices: [Uuid.parse(Constants.UUID_RF_IDEAS_SERVICE_ID)], scanMode: ScanMode.lowLatency) .listen((scanResult) async { if (scanResult.name == Constants.deviceName && scanResult.rssi >= rssiThreshold && scanResult.rssi <= 0) { bool matched = true; for (int i = 0; i < Constants.manufacturerData.length; i++) { if (Constants.manufacturerData[i] != scanResult.manufacturerData[i]) { matched = false; break; } } if (matched) { await scanSubscription.cancel(); completer.complete(BleDevice(scanResult)); } } }, onDone: () { debugPrint("BLE-5 onDone"); debugPrint("BLE-6"); scanSubscription = instance .scanForDevices( requireLocationServicesEnabled: false, withServices: [], //withServices: [Uuid.parse(Constants.UUID_RF_IDEAS_SERVICE_ID)], scanMode: ScanMode.lowLatency) .listen((scanResult) async { if (scanResult.name == Constants.deviceName && scanResult.rssi >= rssiThreshold && scanResult.rssi <= 0) { bool matched = true; for (int i = 0; i < Constants.manufacturerData.length; i++) { if (Constants.manufacturerData[i] != scanResult.manufacturerData[i]) { matched = false; break; } } if (matched) { await scanSubscription.cancel(); completer.complete(BleDevice(scanResult)); } } }); }, onError: (e) { // Catcher.reportCheckedError(e, StackTrace.current); }, cancelOnError: true); Future.delayed(const Duration(seconds: scanningTimeout as int), () async { if (!completer.isCompleted) { debugPrint("BLE-7"); await scanSubscription.cancel(); completer.complete(null); } }); debugPrint("BLE-8"); return completer.future; } catch (e) { // Catcher.reportCheckedError(e, StackTrace.current); return null; } // I am using Background mode Scan time error Assertion failed PluginController reactive_ble_mobile/PluginController.swift:167: Assertion failed
BenelliFurtado commented 4 months ago

@expoders8 , scanning in the background without explicitely specifying the withServices parameter is not possible on iOS and usually also not on current Android versions (maybe on on some older Android versions). Also passing an empty list does not work. (But if it helps: you can connect to the device in the background if you already know its UUID

expoders8 commented 4 months ago

@BenelliFurtado Hello

I don't need to use UUID

Please give me correct solution with code.

BenelliFurtado commented 4 months ago

Hello @expoders8 ,

in Apples Documentation it sais, that the use of UUDIs is necessary for background scanning:

Your app can scan for Bluetooth devices in the background by specifying the bluetooth-central background mode. To do this, your app must explicitly scan for one or more services by specifying them in the serviceUUIDs parameter. The CBCentralManager scan option has no effect while scanning in the background.

So it will work if you uncomment the line from your example code:

scanSubscription = instance
  .scanForDevices(
      requireLocationServicesEnabled: false,
      withServices: [Uuid.parse(Constants.UUID_RF_IDEAS_SERVICE_ID)],
      scanMode: ScanMode.lowLatency)
  .listen((scanResult) async {...});

But of course it works only if your BLE device is advertising at least one of the UUIDs you are filtering for (in its advertising packets). So if your BLE device does not advertise any services, you cannot find it from a background scan on iOS.