using Plugin.BLE; using Plugin.BLE.Abstractions.Contracts; using Plugin.BLE.Abstractions.EventArgs; namespace FieldLogger.Services.Ble; /// A device found during a BLE scan. public sealed record DiscoveredDevice(Guid Id, string Name, int Rssi); /// Thin scan wrapper over Plugin.BLE with per-platform permission handling. public sealed class BleScanner { private readonly IAdapter _adapter = CrossBluetoothLE.Current.Adapter; public event EventHandler? DeviceDiscovered; public bool IsScanning => _adapter.IsScanning; /// /// Scans for BLE devices for the given duration, raising DeviceDiscovered as devices appear. /// Only named devices are reported. /// public async Task ScanAsync(TimeSpan duration, CancellationToken cancellationToken = default) { await EnsurePermissionsAsync(); if (!CrossBluetoothLE.Current.IsOn) throw new InvalidOperationException("Bluetooth is turned off."); void OnDiscovered(object? sender, DeviceEventArgs e) { if (!string.IsNullOrWhiteSpace(e.Device.Name)) DeviceDiscovered?.Invoke(this, new DiscoveredDevice(e.Device.Id, e.Device.Name, e.Device.Rssi)); } _adapter.DeviceDiscovered += OnDiscovered; _adapter.DeviceAdvertised += OnDiscovered; try { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeout.CancelAfter(duration); _adapter.ScanTimeout = (int)duration.TotalMilliseconds; try { await _adapter.StartScanningForDevicesAsync(cancellationToken: timeout.Token); } catch (OperationCanceledException) { // Scan window elapsed or caller cancelled - both are normal. } } finally { _adapter.DeviceDiscovered -= OnDiscovered; _adapter.DeviceAdvertised -= OnDiscovered; if (_adapter.IsScanning) await _adapter.StopScanningForDevicesAsync(); } } /// Requests the runtime permissions BLE scanning needs (Android only; no-op elsewhere). public static async Task EnsurePermissionsAsync() { #if ANDROID if (OperatingSystem.IsAndroidVersionAtLeast(31)) { var status = await Permissions.RequestAsync(); if (status != PermissionStatus.Granted) throw new PermissionException("Bluetooth permission was denied."); } else { var status = await Permissions.RequestAsync(); if (status != PermissionStatus.Granted) throw new PermissionException("Location permission (required for BLE scanning) was denied."); } #else await Task.CompletedTask; #endif } }