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. bool androidLegacy = false,
  13. AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
  14. bool androidUsesFineLocation = false,
  15. bool androidCheckLocationServices = true,
  16. List<Guid> webOptionalServices = const [],
})

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.
  • androidLegacy Android only. If true, scan on 1M phy only. If false, scan on all supported phys. How the radio cycles through all the supported phys is purely dependent on the your Bluetooth stack implementation.
  • androidScanMode choose the android scan mode to use when scanning
  • androidUsesFineLocation request ACCESS_FINE_LOCATION permission at runtime
  • webOptionalServices the optional services for the web target. Required to access device services when scanning without withServices parameter.

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,
  bool androidLegacy = false,
  AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
  bool androidUsesFineLocation = false,
  bool androidCheckLocationServices = true,
  List<Guid> webOptionalServices = const [],
}) 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(!(!kIsWeb && 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,
        androidLegacy: androidLegacy,
        androidScanMode: androidScanMode.value,
        androidUsesFineLocation: androidUsesFineLocation,
        androidCheckLocationServices: androidCheckLocationServices,
        webOptionalServices: webOptionalServices);

    Stream<BmScanResponse> responseStream = FlutterBluePlusPlatform.instance.onScanResponse;

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

    // invoke platform method
    await _invokePlatform(() => FlutterBluePlusPlatform.instance.startScan(settings)).onError((e, s) {
      _stopScan(invokePlatform: false);
      throw e!;
    });

    // check every 250ms for gone devices?
    late Stream<BmScanResponse?> outputStream = removeIfGone != null
        ? _mergeStreams([_scanBuffer!.stream, Stream.periodic(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();
  }
}