startScan method

  1. @override
Future<Stream<BleDevice>> startScan({
  1. List<ScanFilter>? filters,
  2. ScanSettings? settings,
})
override

Starts a scan for nearby Bluetooth Low Energy (BLE) devices and returns a stream of discovered devices.

Scanning for BLE devices is a crucial step in establishing a BLE connection. It allows the mobile app to discover nearby BLE devices and gather essential information like device name, MAC address, and more. This method starts the scanning operation on the platform side and listens for discovered devices.

The function takes optional filters and settings parameters that allow for more targeted device scanning. For example, you could specify a filter to only discover devices that are advertising a specific service. Similarly, settings allows you to adjust aspects like scan mode, report delay, and more.

The method returns a broadcast stream derived from the shared BleMethodChannelRouter. Every time a device is discovered by the native platform, the 'bleDeviceScanned' method is invoked, and the device information is parsed and added to the stream. Starting another BLE operation does not stop this stream from receiving results.

A device is emitted when first discovered and again whenever its advertised content (name, manufacturer data, or advertised service UUIDs) changes during the scan, so the same address can appear more than once. Use the latest emission for an address as its current state. RSSI-only changes are not emitted. See ScanSettings.allowDuplicates for how promptly content changes are noticed on each platform.

Throws a BluetoothScanException if the platform cannot start the scan, for example on Android while Bluetooth is turned off, or a BluetoothPermissionException if the platform reports missing permissions.

Implementation

@override
Future<Stream<BleDevice>> startScan({
  List<ScanFilter>? filters,
  ScanSettings? settings,
}) async {
  // Listen to the platform side for scanned devices. The router delivers scan results to every scan stream, so a
  // concurrent connect, discovery, or status call does not silence this one.
  final Stream<BleDevice> scanStream = BleMethodChannelRouter.forChannel(
    channel,
  ).operationCalls('bleDeviceScanned').transform(
    StreamTransformer<MethodCall, BleDevice>.fromHandlers(
      handleData: (MethodCall call, EventSink<BleDevice> sink) {
        switch (call.method) {
          case 'bleDeviceScanned':
            try {
              BleDevice device;
              // Different operating systems will send the arguments in different formats. So, normalize the
              // arguments as a Map<dynamic, dynamic> for use in the BleDevice.fromMap factory constructor.
              if (call.arguments is String) {
                final Map<dynamic, dynamic> argumentsParsed =
                    json.decode(call.arguments as String)
                        as Map<dynamic, dynamic>;
                device = BleDevice.fromMap(argumentsParsed);
              } else {
                device = BleDevice.fromMap(
                  call.arguments as Map<dynamic, dynamic>,
                );
              }

              // Apply additional filtering on Dart side. Filtering is also done on the native side, but this allows
              // for more complex filtering logic that may not be feasible on the native side. If the device matches
              // the provided filters, add it to the stream.
              if (filters.deviceMatchesFilters(device)) {
                sink.add(device);
              }
            } catch (e) {
              sink.addError(
                FormatException('Failed to parse discovered device info: $e'),
              );
            }
          case BleMethodChannelRouter.errorMethod:
            sink.addError(Exception(call.arguments));
        }
      },
    ),
  );

  // Convert filters and settings into map representations if provided.
  final List<Map<String, dynamic>>? filtersMap =
      filters?.map((filter) => filter.toMap()).toList();
  final Map<String, dynamic>? settingsMap = settings?.toMap();

  // Begin the scan on the platform side, including the filters and settings in the method call if provided.
  try {
    await channel.invokeMethod('startScan', {
      'filters': filtersMap,
      'settings': settingsMap,
    });
  } on PlatformException catch (e) {
    // Handle different error types accordingly. For example, Android reports a BLUETOOTH_UNAVAILABLE error when
    // the scanner cannot be obtained because Bluetooth is turned off.
    if (e.message?.contains('permissions') ?? false) {
      throw BluetoothPermissionException('Permission error: ${e.message}');
    } else {
      throw BluetoothScanException(
        'Failed to start Bluetooth scan: ${e.message}',
      );
    }
  }

  return scanStream;
}