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,
- bool androidLegacy = false,
- AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
- bool androidUsesFineLocation = false,
- bool androidCheckLocationServices = true,
- 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 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.androidLegacy
Android only. Iftrue
, scan on 1M phy only. Iffalse
, 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 scanningandroidUsesFineLocation
requestACCESS_FINE_LOCATION
permission at runtimewebOptionalServices
the optional services for the web target. Required to access device services when scanning withoutwithServices
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();
}
}