flutter_blue icon indicating copy to clipboard operation
flutter_blue copied to clipboard

FlutterBlue.instance.scanResults shows stale results

Open daniels20000 opened this issue 6 years ago • 11 comments

Hi, i hope it won't be redundant. I'm currently facing problem with FlutterBlue.instance.scanResults stream. I'm trying to display/process List of ScanResult and it shows stale results which are no longer available or in range. That's crucial for my application cause I try to make an app which opens doors and there will be potentially dozens of BLE enabled devices. Edit: I've come up with ugly looking quick fix but it's less than ideal: Observable<ScanResult>(FlutterBlue.instance.scan( scanMode: ScanMode(2))).bufferTime(Duration(milliseconds: 500)).asyncMap((value) => value.toSet().toList()..sort((a,b)=> a.device.id.id.compareTo(b.device.id.id)))

daniels20000 avatar Aug 13 '19 06:08 daniels20000

Are you saving the results in a Map? For example Map<DeviceIdentifier, ScanResult> scanResults = Map();. Because then you can just clear the list before every scan scanResults.clear(); And the stale (old) results are gone.

Fleximex avatar Aug 13 '19 10:08 Fleximex

I'm not saving results in a map. My intend is to scan all the time without need to refresh. User is suppose to walk through the building and depending upon rssi threshold app should connect to BLE device and communicate with it.

daniels20000 avatar Aug 13 '19 11:08 daniels20000

Your results should be saved somewhere. How are you using the results if you don't have them stored somewhere?

The Flutter Blue code actually does seem to clear the _scanResults list before every scan: _scanResults.add(<ScanResult>[]); You could also make your scan recursive. Set a timeout and after the scan is completed you call scan again.

Scanning can be done asynchronous. So you could also keep scanning endlessly and then every (i.e.) 3 seconds just clear a local scan result list. Or maybe better for your situation. Save the results in a list (i.e. called 'current list') and then after 3 seconds copy it to another list (i.e. called 'check list'). Then compare both lists. And (after 3 seconds) if a result from check list is not present in current list you remove it from check list. The only thing is that you need to use check list so you are 3 seconds behind the actual scan results.

Maybe you can share your full scan code.

Fleximex avatar Aug 13 '19 11:08 Fleximex

I think I'm gonna put that stream of scan results Observable<ScanResult>(FlutterBlue.instance.scan( withServices: [Guid(myUUID)], scanMode: ScanMode(2))) .bufferTime(Duration(milliseconds: 500)).distinct((p,n)=> p.isEmpty&&n.isEmpty) .asyncMap((value) => value.toSet().toList() ..sort((a, b) => a.device.id.id.compareTo(b.device.id.id))) which every 500ms yields me fresh List of buffered ScanResults into Bloc and if any rssi will be under threshold I'll attempt to connect to it.

daniels20000 avatar Aug 13 '19 12:08 daniels20000

The Flutter Blue code actually clear the _scanResults list just on scan start:

Stream<ScanResult> scan({
    ScanMode scanMode = ScanMode.lowLatency,
    List<Guid> withServices = const [],
    List<Guid> withDevices = const [],
    Duration timeout,
  }) async* {
    var settings = protos.ScanSettings.create()
      ..androidScanMode = scanMode.value
      ..serviceUuids.addAll(withServices.map((g) => g.toString()).toList());

    if (_isScanning.value == true) {
      throw Exception('Another scan is already in progress.');
    }
    ....
    // Clear scan results list
    _scanResults.add(<ScanResult>[]);

    try {
      await _channel.invokeMethod('startScan', settings.writeToBuffer());
    } catch (e) {
      print('Error starting scan.');
      _stopScanPill.add(null);
      _isScanning.add(false);
      throw e;
    }
    ....
  } 

The list is no longer cleaned during the scan. The items are just added or updated, so it contains all devices that are no longer available:

yield* Observable(FlutterBlue.instance._methodStream
            .where((m) => m.method == "ScanResult")
            .map((m) => m.arguments))
        .takeUntil(Observable.merge(killStreams))
        .doOnDone(stopScan)
        .map((buffer) => new protos.ScanResult.fromBuffer(buffer))
        .map((p) {
      final result = new ScanResult.fromProto(p);
      final list = _scanResults.value;
      int index = list.indexOf(result);
      if (index != -1) {
        list[index] = result;
      } else {
        list.add(result);
      }
      _scanResults.add(list);
      return result;
    });

giuseppe-v avatar Oct 04 '19 15:10 giuseppe-v

Could be useful a public method on FlutterBlue class that we can use to reset the scanResults Stream. Something like that:

void resetScanResult(){
    // Clear scan results list
    _scanResults.add(<ScanResult>[]);
  }

giuseppe-v avatar Oct 04 '19 15:10 giuseppe-v

I'm a bit hesitant to expose list manipulation methods on the API, as it could get a bit crowded.

This might be better handled in the end application.

I'm open to checking out PR's for this.

pauldemarco avatar Oct 18 '19 08:10 pauldemarco

How about a method which sets a timeout for every device after which it would be removed unless it would be still in range. I try to avoid clearing list completely and rebuilding it up from scratch.

daniels20000 avatar Oct 22 '19 18:10 daniels20000

yes, this actually becomes an issue when we listen to scanResults asynchronously. Say the start and stop scan methods are invoked only on widget creation and dispose, listening to scanResults stream sends stale data (i.e) sends devices that are just discovered from beginning of scan even though they are not in range anymore.

itsgk92 avatar May 12 '20 14:05 itsgk92

Temporary decision

await FlutterBlue.instance.startScan(timeout: Duration(milliseconds: 1));
    await Future.delayed(Duration(milliseconds: 3));

    FlutterBlue.instance.scanResults.listen((snapshot) {
      // do smth
    });
    await FlutterBlue.instance.startScan(timeout: Duration(seconds: 4));

Pavel-Gorokhov avatar Feb 24 '21 19:02 Pavel-Gorokhov

any updates? @daniels20000

jakubkrnac avatar Jul 27 '22 11:07 jakubkrnac