ble_proximity_signal
Developed with 💙 by Very Good Ventures 🦄
ble_proximity_signal is a foreground-only BLE proximity module for a “metal detector” style UX.
- Broadcast: advertise a short token
- Scan: scan nearby tokens (up to 5 targets) and track signal strength
- Signal processing: smooth RSSI (EMA) and map it to intensity (0..1) + proximity levels
This package contains:
- The public Dart API (
BleProximitySignal) - Signal processing (smoothing/hysteresis/intensity mapping)
- Shared models used by the example app
This plugin is designed for immediate feedback while the app is in the foreground. It does not claim background reliability or distance-in-meters.
Quickstart
1) Start broadcasting a token
final ble = BleProximitySignal();
await ble.startBroadcast(token: 'a1b2c3d4'); // hex or base64url/base64
2) Scan and listen to proximity events
final ble = BleProximitySignal();
await ble.startScan(targetTokens: ['a1b2c3d4']); // max 5 tokens
final sub = ble.events.listen((event) {
// event.intensity: 0..1
// event.level: far / near / veryNear
// event.rssi + event.smoothRssi
print('level=${event.level} intensity=${event.intensity} rssi=${event.rssi}');
});
Stop when done:
await sub.cancel();
await ble.stopScan();
await ble.stopBroadcast();
await ble.dispose();
Permissions and availability
Request permissions and confirm the adapter is usable before you scan or broadcast:
final ble = BleProximitySignal();
// Ask for the Bluetooth permissions the plugin needs.
final status = await ble.requestPermissions();
if (status != BlePermissionStatus.granted) {
// denied / permanentlyDenied / restricted / notDetermined
return;
}
// Confirm the adapter is powered on and authorized.
final availability = await ble.checkAvailability();
if (availability != BleAvailability.ready) {
// poweredOff / unauthorized / unsupported / unknown
return;
}
// React to the adapter being toggled off/on while running.
final availSub = ble.availabilityChanges.listen((a) => print('BLE: $a'));
Use checkPermissions() for a non-prompting status read. On Android it reports
denied instead of permanentlyDenied (the rationale state is only known after
requestPermissions()).
Token format
token (broadcast) and targetTokens (scan) accept:
- Hex string (even length):
a1b2c3d4 - base64url / base64 (padding optional): decoded and normalized to hex internally
Normalization behavior:
- hex input is lowercased
- base64url/base64 input is decoded to bytes and converted to hex lowercase
If the token cannot be parsed, the Dart wrapper throws FormatException.
API overview
BleProximitySignal
startBroadcast(token:, config:)/stopBroadcast()startScan(targetTokens:, config:, signalConfig:)/stopScan()requestPermissions()/checkPermissions()→BlePermissionStatusRequest or read the BLE runtime permissions the plugin needs.checkAvailability()→BleAvailability;Stream<BleAvailability> get availabilityChangesWhether the adapter is powered on/authorized, plus live changes.Stream<ProximityEvent> get eventsSmoothed/hysteresis-applied stream intended for UX.Stream<RawScanResult> get rawScanResultsDebug-only. Raw scan events coming from native.rawScanLog(maxEntries:)Debug buffer utility.debugDiscoverServices(deviceId:)Debug helper: connect + dump services/characteristics.
ProximityEvent (high level event)
intensity(0..1)level(far/near/veryNear)rssi,smoothRssideviceId,deviceName, etc. (best-effort, platform dependent)
Configuration
ScanConfig (native filtering)
serviceUuid(default:BroadcastConfig.defaultServiceUuid)debugAllowAll(if true, scan all peripherals without UUID/token filtering)
SignalConfig (Dart-side signal processing)
Controls EMA smoothing + thresholds/hysteresis (see source).
Limitations / Non-goals
- Foreground-only (no background guarantees)
- No distance-in-meters
- No rolling IDs
- No built-in sound/vibration/notifications (left to the app layer)
Example app
The example app (example/) provides:
- Start/stop scan
- Start/stop broadcast
- “metal detector” UX using intensity (beep/visual)
- Debug mode: scan all peripherals + visible device list
- Debug probe: tap a device to run a service/characteristic discovery dump
Run:
cd example
flutter run
Integration tests đź§Ş
This package supports two integration testing tools in the example app:
Fluttium (Simple UI Tests)
Fluttium provides YAML-based declarative tests for quick UI smoke testing.
Install the CLI: fluttium_cli install guide
Run the flow:
cd example
fluttium test flows/test_platform_name.yaml
Note: the provided flow is a UI smoke test for the example app. If you change UI labels/buttons, update the flow steps accordingly.
Patrol (BLE Permission Tests)
Patrol provides native automation capabilities, essential for testing BLE permissions.
Install the CLI:
dart pub global activate patrol_cli
Run tests:
cd example
# On simulator
patrol test -t integration_test/patrol_test.dart -d "iPhone 17"
# On real device (recommended for BLE)
patrol test -t integration_test/patrol_test.dart
Why both? Fluttium is perfect for quick UI smoke tests, but cannot handle native permission dialogs. Patrol is used for comprehensive BLE testing including permission scenarios.
See INTEGRATION_TESTING.md for a detailed comparison.
Troubleshooting (fast path)
- Bluetooth ON?
- Permissions granted? (Android 12+ requires runtime permissions for scan/connect)
- Device supports BLE advertising? (some devices do not)
- If scan/broadcast silently fails, try toggling Bluetooth and re-run.
Permissions (practical)
Permissions vary by OS version and vendor.
Android 12+
BLUETOOTH_SCANBLUETOOTH_CONNECTBLUETOOTH_ADVERTISE
These require runtime permission prompts on modern Android.
iOS
NSBluetoothAlwaysUsageDescriptioninInfo.plist

