flutterbluetoothbluetooth-lowenergybeacon

Flutter BLE Scanning Issue – Missing Advertised Slots from W6 BLE Beacon


What I’m Trying to Achieve

I’m developing a Flutter app for beacon proximity marketing using the W6 BLE beacon. This device has six configurable slots that can contain:

The beacon advertises these slot data sequentially. My app scans for nearby beacon devices and parses their advertisement data.

The Problem

Even though the device is configured to advertise all six slots, my Flutter app is only detecting two slots:

To verify if all slot data is actually being advertised, I tested with:

These apps successfully detect all advertised slot data, confirming that the beacon is working correctly.

My Approaches So Far

I have tried the following methods, but they all return only the two mentioned slots:

  1. flutter_reactive_ble
  2. flutter_blue_plus
  3. Android-Nordic-SDK (via Platform Channels)

Code Snippets

1️⃣ Using flutter_reactive_ble

Future<Stream<List<DiscoveredDevice>>> scanForDevices() async {
    await ensureBluetoothReady();

    final rawScanStream = _flutterReactiveBle.scanForDevices(
      withServices: [],
      scanMode: ScanMode.balanced,
      requireLocationServicesEnabled: true,
    );

    final transformedStream = rawScanStream.scan<List<DiscoveredDevice>>(
      (List<DiscoveredDevice> accumulator, DiscoveredDevice device, int index) {
        final deviceIndex = accumulator.indexWhere((d) => d.id == device.id);

        if (deviceIndex >= 0) {
            accumulator[deviceIndex] = device;
        } else {
            accumulator.add(device);
        }

        return accumulator;
      },
      <DiscoveredDevice>[],
    );

    _scanSubscription = transformedStream.listen(
      (devices) => _scanController.add(devices),
      onError: (error) => _scanController.addError(error),
      onDone: () => _scanController.close(),
    );

    return _scanController.stream;
  }

void parseBeaconData(DiscoveredDevice device) {
    device.serviceData.forEach((serviceUuid, dataBytes) {
      String uuidLower = serviceUuid.toString().toLowerCase();
      if (uuidLower.contains("feaa") || uuidLower.contains("feab")) {
        final List<int> bytes = dataBytes.toList();
        if (bytes.isEmpty) return;
        
        int frameType = bytes[0];
        
        switch (frameType) {
          case 0x00: // Eddystone-UID
            break;
          case 0x10: // Eddystone-URL
            break;
          case 0x20: // Eddystone-TLM
            break;
            case 0x40: // Device Information
            break;
          case 0x50: // iBeacon
            break;
          case 0x60: // 3-axis Acc
            break;
          default:
            break; // Ignore unknown frame types
        }
      }
    });
  }

Future<bool> ensureBluetoothReady() async {
    if (!Platform.isAndroid) {
      final bluetoothStatus = await Permission.bluetooth.status;
      if (!bluetoothStatus.isGranted) {
        final requestedBluetoothStatus = await Permission.bluetooth.request();
        if (!requestedBluetoothStatus.isGranted) {
          throw Exception(
              'Bluetooth permission is required. Please enable Bluetooth permission in app settings.');
        }
      }
    }

    final bluetoothScanStatus = await Permission.bluetoothScan.status;
    if (!bluetoothScanStatus.isGranted) {
      final requestedBluetoothScanStatus =
          await Permission.bluetoothScan.request();
      if (!requestedBluetoothScanStatus.isGranted) {
        throw Exception(
            'Bluetooth Scan permission is required. Please enable Bluetooth Scan permission in app settings.');
      }
    }

    final bluetoothConnectStatus = await Permission.bluetoothConnect.status;
    if (!bluetoothConnectStatus.isGranted) {
      final requestedBluetoothConnectStatus =
          await Permission.bluetoothConnect.request();
      if (!requestedBluetoothConnectStatus.isGranted) {
        throw Exception(
            'Bluetooth Connect permission is required. Please enable Bluetooth Connect permission in app settings.');
      }
    }

    final locationStatus = await Permission.locationWhenInUse.status;
    if (!locationStatus.isGranted) {
      final requestedLocationStatus =
          await Permission.locationWhenInUse.request();
      if (!requestedLocationStatus.isGranted) {
        throw Exception(
            'Location permission is required. Please enable Location permission in app settings.');
      }
    }

    final currentBleStatus = await _flutterReactiveBle.statusStream.first;
    if (currentBleStatus != BleStatus.ready) {
      if (Platform.isAndroid) {
        final AndroidIntent intent = AndroidIntent(
          action: 'android.bluetooth.adapter.action.REQUEST_ENABLE',
          flags: [Flag.FLAG_ACTIVITY_NEW_TASK],
        );
        await intent.launch();
      } else {
        await openAppSettings();
      }
    }

    return true;
  }

2️⃣ Using flutter_blue_plus

Future<void> scanForDevices() async {
    await ensureBluetoothReady();

    FlutterBluePlus.startScan(
      withServices: [],
      withRemoteIds: ['C3:21:47:A2:A9:40'],
      androidScanMode: AndroidScanMode.lowLatency,
      oneByOne: true,
      continuousUpdates: true,
      continuousDivisor: 1,
      removeIfGone: null,
      androidUsesFineLocation: true,
    );

    FlutterBluePlus.scanResults.listen((List<ScanResult> scanResults) {
      for (final result in scanResults) {
        _parseAdvertisement(result.advertisementData);
      }
    });
}

3️⃣ Using Android-Nordic-SDK via Platform Channels

Android (Java) code:

public class BeaconScanner implements MethodChannel.MethodCallHandler {
    private static final String TAG = "BeaconScanner";
    private final Context context;
    private final MokoBleScanner scanner;
    private final MethodChannel channel;

    public BeaconScanner(Context context, MethodChannel channel) {
        this.context = context;
        this.channel = channel;
        this.scanner = new MokoBleScanner(context);
    }

    public void startScanning() {
        scanner.startScanDevice(new MokoScanDeviceCallback() {
            @Override
            public void onStartScan() {
                channel.invokeMethod("onScanStart", null);
            }

            @Override

            public void onScanDevice(DeviceInfo device) {
                Map<String, Object> beaconData = new HashMap<>();

                beaconData.put("mac", device.mac);
                beaconData.put("rssi", device.rssi);
                beaconData.put("name", device.name);
                beaconData.put("scanRecord", device.scanRecord);

                new Handler(Looper.getMainLooper()).post(() -> {
                    channel.invokeMethod("onBeaconFound", beaconData);
                });
            }

            @Override
            public void onStopScan() {
                channel.invokeMethod("onScanStop", null);
            }
        });
        Log.d(TAG, "Beacon scanning started.");
    }

    public void stopScanning() {
        scanner.stopScanDevice();
        Log.d(TAG, "Beacon scanning stopped.");
    }

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
        if ("startScan".equals(call.method)) {
            startScanning();
            result.success(null);
        } else if ("stopScan".equals(call.method)) {
            stopScanning();
            result.success(null);
        } else {
            result.notImplemented();
        }
    }
}

Flutter (Dart) code:

class BeaconScanner {
  static const MethodChannel _channel = MethodChannel('beacon_scanner');
  static final StreamController<Map<String, dynamic>> _beaconStreamController = StreamController.broadcast();

  static Stream<Map<String, dynamic>> get beaconStream => _beaconStreamController.stream;

  static Future<void> startScan() async {
    try {
      await _channel.invokeMethod('startScan');
    } catch (e) {
      print("Error starting beacon scan: $e");
    }
  }

  static void _handleBeaconFound(dynamic data) {
    if (data is Map<String, dynamic>) {
      _beaconStreamController.add(data);
    }
  }

  static void init() {
    _channel.setMethodCallHandler((MethodCall call) async {
      if (call.method == "onBeaconFound") {
        _handleBeaconFound(call.arguments);
      }
    });
  }
}

Permissions in Android Manifest

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

What I Need Help With

I’d really appreciate any insights, suggestions, or alternative approaches! Thanks in advance.


Solution

  • The issue was related to the BLUETOOTH_SCAN permission in the AndroidManifest.xml file. Initially, I had set the neverForLocation flag for this permission, assuming it would not affect scanning results. However, after some research, I discovered that certain BLE advertisement slots (like Eddystone-UID, Eddystone-URL, and iBeacon) require location-related permissions to be properly detected.

    Solution:

    I followed the flutter_reactive_ble documentation and updated my AndroidManifest.xml file as follows:

    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" 
                     tools:remove="android:usesPermissionFlags"
                     tools:targetApi="s" />
    

    This removed the restrictive flag and allowed my app to scan for all advertised slots correctly.

    Key Takeaways:

    After applying this change, my Flutter app successfully detected all six advertised slots from my W6 BLE beacon! 🚀

    Hope this helps anyone facing a similar issue.