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.
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.
I have tried the following methods, but they all return only the two mentioned slots:
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;
}
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);
}
});
}
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);
}
});
}
}
<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"/>
0x40
and 0x60
) when the beacon advertises all six slots?I’d really appreciate any insights, suggestions, or alternative approaches! Thanks in advance.
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.
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.
The neverForLocation
flag can prevent scanning from working as expected.
Removing the flag using tools:remove="android:usesPermissionFlags"
resolved the issue.
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.