Scanner
Overview
The Scanner is your entry point to discovering Bluetooth Low Energy (BLE) devices nearby. Think of it as a radar that continuously listens for BLE advertisement packets broadcast by devices in range.
Interface: IBluetoothScanner
What Does It Do?
The Scanner allows you to:
- Start and stop scanning for nearby BLE devices
- Filter devices based on various criteria (name, service UUIDs, signal strength)
- Receive real-time advertisement data from devices
- Manage Bluetooth permissions
Basic Workflow
┌─────────────┐ ┌──────────────┐ ┌────────────────┐
│ Request │─────▶│ Start │─────▶│ Receive │
│ Permissions │ │ Scanning │ │ Advertisements │
└─────────────┘ └──────────────┘ └────────────────┘
│
▼
┌────────────────┐
│ Stop Scanning │
└────────────────┘
Getting Started
1. Request Permissions
Before scanning, you need the user's permission to use Bluetooth:
// scanner is an IBluetoothScanner obtained via constructor injection
// (see Docs/Configuration/Dependency-Injection.md) — it's registered as a
// singleton, so the same instance is shared across your app.
// Check if we already have permissions
bool hasPermission = await scanner.HasScannerPermissionsAsync();
if (!hasPermission)
{
// Request permissions from the user
await scanner.RequestScannerPermissionsAsync();
}
Platform Notes:
- Android: Requests
BLUETOOTH_SCANpermission (API 31+) or location permissions (older versions) - iOS/macOS: Requests "Bluetooth Always" permission
- Windows: Checks adapter availability and radio state
2. Start Scanning
Once you have permissions, start scanning:
// Start scanning with default settings
await scanner.StartScanningAsync();
// Or with custom options
var options = new ScanningOptions
{
ScanMode = BluetoothScanMode.LowLatency, // Fast discovery
IgnoreDuplicateAdvertisements = false, // Get all updates
ServiceUuids = new[] { myServiceGuid } // Filter by service
};
await scanner.StartScanningAsync(options);
3. Listen for Advertisements
scanner.AdvertisementReceived += (sender, args) =>
{
IBluetoothAdvertisement ad = args.Advertisement;
Console.WriteLine($"Found: {ad.DeviceName}");
Console.WriteLine($"Signal: {ad.SignalStrengthInDBm} dBm");
Console.WriteLine($"Address: {ad.BluetoothAddress}");
};
4. Stop Scanning
await scanner.StopScanningAsync();
Scanning Options
The ScanningOptions class lets you customize your scanning behavior:
Basic Filtering
var options = new ScanningOptions
{
// Ignore devices without a name
IgnoreNamelessAdvertisements = true,
// Only report each device once (not every advertisement)
IgnoreDuplicateAdvertisements = true,
// Custom filter function
AdvertisementFilter = ad =>
ad.DeviceName.Contains("Sensor") &&
ad.SignalStrengthInDBm > -70
};
Service UUID Filtering
Only discover devices advertising specific services:
var options = new ScanningOptions
{
ServiceUuids = new[]
{
Guid.Parse("0000180F-0000-1000-8000-00805F9B34FB"), // Battery Service
Guid.Parse("0000180A-0000-1000-8000-00805F9B34FB") // Device Info
}
};
Scan Mode (Power vs Speed)
Choose the right balance for your use case:
// Fast discovery, higher power consumption
BluetoothScanMode.LowLatency
// Balanced performance (default)
BluetoothScanMode.Balanced
// Battery friendly, slower discovery
BluetoothScanMode.LowPower
// Only scan when other apps are scanning (Android only)
BluetoothScanMode.Opportunistic
Signal Strength Filtering
Only detect nearby devices:
var options = new ScanningOptions
{
RssiThreshold = -70 // Typical values: -100 (far) to -30 (very close)
};
RSSI Guide:
-30 to -50 dBm: Excellent signal (very close)-50 to -70 dBm: Good signal (nearby)-70 to -90 dBm: Weak signal (far)-90 to -100 dBm: Very weak signal (edge of range)
Events
The Scanner provides several events to track its state:
// Scanning lifecycle events
scanner.Starting += (s, e) => Console.WriteLine("Scanner is starting...");
scanner.Started += (s, e) => Console.WriteLine("Scanner started!");
scanner.Stopping += (s, e) => Console.WriteLine("Scanner is stopping...");
scanner.Stopped += (s, e) => Console.WriteLine("Scanner stopped!");
scanner.RunningStateChanged += (s, e) => Console.WriteLine($"Running: {scanner.IsRunning}");
// Advertisement received
scanner.AdvertisementReceived += (s, args) =>
{
// Process advertisement
};
State Properties
Monitor the scanner's current state:
bool isRunning = scanner.IsRunning; // Is actively scanning?
bool isStarting = scanner.IsStarting; // Is currently starting?
bool isStopping = scanner.IsStopping; // Is currently stopping?
Advanced Features
Dynamic Options Updates
Change scanning options without stopping:
await scanner.UpdateScannerOptionsAsync(new ScanningOptions
{
ScanMode = BluetoothScanMode.LowPower // Switch to battery-saving mode
});
Safe Start/Stop
Use the "IfNeeded" variants to avoid exceptions:
// Only starts if not already running
await scanner.StartScanningIfNeededAsync();
// Only stops if currently running
await scanner.StopScanningIfNeededAsync();
Clean Restart
CleanRestartScanningAsync stops the scanner, discards every device in the registry, then starts scanning again:
await scanner.CleanRestartScanningAsync();
Use it when a device changes identity while the scanner is running - typically a device rebooting into or out of firmware-update mode, where a stale registry entry plus an in-flight scan session stop it from being rediscovered under its new advertisement. It also accepts a replacement advertisement filter, applied while the scanner is stopped so it is already in effect when scanning resumes:
await scanner.CleanRestartScanningAsync(ad => ad.DeviceName.Contains("DfuTarg"));
Every device is disconnected, removed and disposed, so IBluetoothRemoteDevice instances do not survive the restart.
Keep the device id instead and re-acquire the instance afterwards:
var deviceId = device.Id;
await scanner.CleanRestartScanningAsync();
device = await scanner.WaitForDeviceToAppearAsync(deviceId, TimeSpan.FromSeconds(30));
Timeouts and Cancellation
All operations support timeouts and cancellation:
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(10));
try
{
await scanner.StartScanningAsync(
options: null,
timeout: TimeSpan.FromSeconds(5),
cancellationToken: cts.Token
);
}
catch (TimeoutException)
{
Console.WriteLine("Scanner took too long to start");
}
catch (OperationCanceledException)
{
Console.WriteLine("Scan was cancelled");
}
Common Patterns
Simple Device Discovery
// The scanner already tracks discovered devices for you — no need to
// collect them manually from the event args.
scanner.AdvertisementReceived += (s, args) =>
{
Console.WriteLine($"Discovered: {args.Advertisement.DeviceName}");
};
await scanner.StartScanningAsync(new ScanningOptions
{
IgnoreDuplicateAdvertisements = true
});
// Scan for 10 seconds
await Task.Delay(TimeSpan.FromSeconds(10));
await scanner.StopScanningAsync();
var foundDevices = scanner.GetDevices();
Console.WriteLine($"Found {foundDevices.Count} devices");
Find Specific Device
var targetDevice = await FindDeviceByNameAsync("MySensor");
async Task<IBluetoothRemoteDevice> FindDeviceByNameAsync(string name)
{
// WaitForDeviceToAppearAsync does the wait-with-timeout/filter dance for you.
await scanner.StartScanningAsync();
try
{
return await scanner.WaitForDeviceToAppearAsync(
device => device.Name == name,
timeout: TimeSpan.FromSeconds(30));
}
finally
{
await scanner.StopScanningAsync();
}
}
Monitor Signal Strength
scanner.AdvertisementReceived += (s, args) =>
{
var rssi = args.Advertisement.SignalStrengthInDBm;
var quality = rssi switch
{
>= -50 => "Excellent",
>= -70 => "Good",
>= -90 => "Weak",
_ => "Very Weak"
};
Console.WriteLine($"{args.Advertisement.DeviceName}: {rssi} dBm ({quality})");
};
Best Practices
Always Stop Scanning: Scanning drains battery - stop when you're done
try { await scanner.StartScanningAsync(); // Do work... } finally { await scanner.StopScanningIfNeededAsync(); }Use Appropriate Scan Mode: Choose based on your needs
- Quick discovery? Use
LowLatency - Background monitoring? Use
LowPower - Most cases? Use
Balanced(default)
- Quick discovery? Use
Filter Early: Use
ScanningOptionsto filter at the system level rather than in your handlerHandle Permissions Properly: Always check and request permissions before scanning
Don't Dispose It Yourself:
IBluetoothScanneris registered as a DI singleton (services.AddSingleton<IBluetoothScanner, BluetoothScanner>()), so the container owns its lifetime and disposal — just take it via constructor injection and callStopScanningAsync()/StopScanningIfNeededAsync()when you're done scanning.
Troubleshooting
No Devices Found
- Check permissions are granted
- Ensure Bluetooth is enabled on the device
- Move closer to the target device
- Check if service UUID filtering is too restrictive
- Try
ScanMode.LowLatencyfor faster discovery
Too Many Duplicate Advertisements
Set IgnoreDuplicateAdvertisements = true in ScanningOptions
Battery Drain
- Use
ScanMode.LowPower - Stop scanning when not needed
- Use service UUID filtering to reduce processing
Scanner Won't Start
- Check permissions:
await scanner.HasScannerPermissionsAsync() - Ensure Bluetooth adapter is available
- Check if already running:
scanner.IsRunning
Related Topics
- Device - Connect to discovered devices
- Advertisement - Understanding advertisement data
- Service - Explore device services after connection