startScan static method
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,
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 serviceswithRemoteIds
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 datawithServiceData
filter by service datatimeout
calls stopScan after a specified durationremoveIfGone
if true, remove devices after they've stopped advertising for X durationcontinuousUpdates
Iftrue
, 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 forcontinuousUpdates
mode.oneByOne
iftrue
, we will stream every advertistment one by one, possibly including duplicates. Iffalse
, we deduplicate the advertisements, and return a list of devices.androidScanMode
choose the android scan mode to use when scanningandroidUsesFineLocation
requestACCESS_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();
}
}