startScan static method

Future<void> startScan({
  1. List<Guid> withServices = const [],
  2. List<String> withRemoteIds = const [],
  3. List<String> withNames = const [],
  4. List<String> withKeywords = const [],
  5. List<MsdFilter> withMsd = const [],
  6. List<ServiceDataFilter> withServiceData = const [],
  7. Duration? timeout,
  8. Duration? removeIfGone,
  9. bool continuousUpdates = false,
  10. int continuousDivisor = 1,
  11. bool oneByOne = false,
  12. AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
  13. bool androidUsesFineLocation = false,
})

Start a scan, and return a stream of results Note: scan filters use an "or" behavior. i.e. if you set withServices & withNames we return all the advertisments that match any of the specified services or any of the specified names.

  • withServices filter by advertised services
  • withRemoteIds filter for known remoteIds (iOS: 128-bit guid, android: 48-bit mac address)
  • withNames filter by advertised names (exact match)
  • withKeywords filter by advertised names (matches any substring)
  • withMsd filter by manfacture specific data
  • withServiceData filter by service data
  • timeout calls stopScan after a specified duration
  • removeIfGone if true, remove devices after they've stopped advertising for X duration
  • continuousUpdates If true, we continually update 'lastSeen' & 'rssi' by processing duplicate advertisements. This takes more power. You typically should not use this option.
  • continuousDivisor Useful to help performance. If divisor is 3, then two-thirds of advertisements are ignored, and one-third are processed. This reduces main-thread usage caused by the platform channel. The scan counting is per-device so you always get the 1st advertisement from each device. If divisor is 1, all advertisements are returned. This argument only matters for continuousUpdates mode.
  • oneByOne if true, we will stream every advertistment one by one, possibly including duplicates. If false, we deduplicate the advertisements, and return a list of devices.
  • androidScanMode choose the android scan mode to use when scanning
  • androidUsesFineLocation request ACCESS_FINE_LOCATION permission at runtime

Implementation

static Future<void> startScan({
  List<Guid> withServices = const [],
  List<String> withRemoteIds = const [],
  List<String> withNames = const [],
  List<String> withKeywords = const [],
  List<MsdFilter> withMsd = const [],
  List<ServiceDataFilter> withServiceData = const [],
  Duration? timeout,
  Duration? removeIfGone,
  bool continuousUpdates = false,
  int continuousDivisor = 1,
  bool oneByOne = false,
  AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
  bool androidUsesFineLocation = false,
}) async {
  // check args
  assert(removeIfGone == null || continuousUpdates, "removeIfGone requires continuousUpdates");
  assert(removeIfGone == null || !oneByOne, "removeIfGone is not compatible with oneByOne");
  assert(continuousDivisor >= 1, "divisor must be >= 1");

  // check filters
  bool hasOtherFilter = withServices.isNotEmpty ||
      withRemoteIds.isNotEmpty ||
      withNames.isNotEmpty ||
      withMsd.isNotEmpty ||
      withServiceData.isNotEmpty;

  // Note: `withKeywords` is not compatible with other filters on android
  // because it is implemented in custom fbp code, not android code, and the
  // android 'name' filter is only available as of android sdk 33 (August 2022)
  assert(!(Platform.isAndroid && withKeywords.isNotEmpty && hasOtherFilter),
      "withKeywords is not compatible with other filters on Android");

  // only allow a single task to call
  // startScan or stopScan at a time
  _Mutex mtx = _MutexFactory.getMutexForKey("scan");
  await mtx.take();
  try {
    // already scanning?
    if (_isScanning.latestValue == true) {
      // stop existing scan
      await _stopScan();
    }

    // push to stream
    _isScanning.add(true);

    var settings = BmScanSettings(
        withServices: withServices,
        withRemoteIds: withRemoteIds,
        withNames: withNames,
        withKeywords: withKeywords,
        withMsd: withMsd.map((d) => d._bm).toList(),
        withServiceData: withServiceData.map((d) => d._bm).toList(),
        continuousUpdates: continuousUpdates,
        continuousDivisor: continuousDivisor,
        androidScanMode: androidScanMode.value,
        androidUsesFineLocation: androidUsesFineLocation);

    Stream<BmScanResponse> responseStream = FlutterBluePlus._methodStream.stream
        .where((m) => m.method == "OnScanResponse")
        .map((m) => m.arguments)
        .map((args) => BmScanResponse.fromMap(args));

    // Start listening now, before invokeMethod, so we do not miss any results
    _scanBuffer = _BufferStream.listen(responseStream);

    // invoke platform method
    await _invokeMethod('startScan', settings.toMap()).onError((e, s) => _stopScan(invokePlatform: false));

    // check every 250ms for gone devices?
    late Stream<BmScanResponse?> outputStream = removeIfGone != null
        ? _mergeStreams([_scanBuffer!.stream, Stream.periodic(const Duration(milliseconds: 250))])
        : _scanBuffer!.stream;

    // start by pushing an empty array
    _scanResults.add([]);

    List<ScanResult> output = [];

    // listen & push to `scanResults` stream
    _scanSubscription = outputStream.listen((BmScanResponse? response) {
      if (response == null) {
        // if null, this is just a periodic update to remove old results
        if (output._removeWhere((elm) => DateTime.now().difference(elm.timeStamp) > removeIfGone!)) {
          _scanResults.add(List.from(output)); // push to stream
        }
      } else {
        // failure?
        if (response.success == false) {
          var e = FlutterBluePlusException(_nativeError, "scan", response.errorCode, response.errorString);
          _scanResults.addError(e);
          _stopScan(invokePlatform: false);
        }

        // iterate through advertisements
        for (BmScanAdvertisement bm in response.advertisements) {
          // cache platform name
          if (bm.platformName != null) {
            _platformNames[bm.remoteId] = bm.platformName!;
          }

          // cache advertised name
          if (bm.advName != null) {
            _advNames[bm.remoteId] = bm.advName!;
          }

          // convert
          ScanResult sr = ScanResult.fromProto(bm);

          if (oneByOne) {
            // push single item
            _scanResults.add([sr]);
          } else {
            // add result to output
            output.addOrUpdate(sr);
          }
        }

        // push entire list
        if (!oneByOne) {
          _scanResults.add(List.from(output));
        }
      }
    });

    // Start timer *after* stream is being listened to, to make sure the
    // timeout does not fire before _scanSubscription is set
    if (timeout != null) {
      _scanTimeout = Timer(timeout, stopScan);
    }
  } finally {
    mtx.give();
  }
}