Fix macOS Bluetooth state and add RTK debug console

This commit is contained in:
Brent Perteet
2026-07-10 20:48:42 -05:00
parent 6404cd9a7e
commit 436633568c
4 changed files with 116 additions and 19 deletions

View File

@@ -10,6 +10,7 @@ public sealed record DiscoveredDevice(Guid Id, string Name, int Rssi);
/// <summary>Thin scan wrapper over Plugin.BLE with per-platform permission handling.</summary>
public sealed class BleScanner
{
private readonly IBluetoothLE _bluetooth = CrossBluetoothLE.Current;
private readonly IAdapter _adapter = CrossBluetoothLE.Current.Adapter;
public event EventHandler<DiscoveredDevice>? DeviceDiscovered;
@@ -23,9 +24,7 @@ public sealed class BleScanner
public async Task ScanAsync(TimeSpan duration, CancellationToken cancellationToken = default)
{
await EnsurePermissionsAsync();
if (!CrossBluetoothLE.Current.IsOn)
throw new InvalidOperationException("Bluetooth is turned off.");
await EnsureBluetoothReadyAsync(cancellationToken);
void OnDiscovered(object? sender, DeviceEventArgs e)
{
@@ -58,6 +57,65 @@ public sealed class BleScanner
}
}
private async Task EnsureBluetoothReadyAsync(CancellationToken cancellationToken)
{
// CoreBluetooth commonly starts in Unknown while CBCentralManager is being
// initialized. IsOn is false for every state except On, so checking it
// immediately incorrectly reports that Bluetooth is switched off on macOS.
if (_bluetooth.State is BluetoothState.Unknown or BluetoothState.TurningOn)
{
var stateChanged = new TaskCompletionSource<BluetoothState>(
TaskCreationOptions.RunContinuationsAsynchronously);
void OnStateChanged(object? sender, BluetoothStateChangedArgs args)
{
if (args.NewState is not (BluetoothState.Unknown or BluetoothState.TurningOn))
stateChanged.TrySetResult(args.NewState);
}
_bluetooth.StateChanged += OnStateChanged;
try
{
// Recheck after subscribing so a transition cannot be missed.
var currentState = _bluetooth.State;
if (currentState is BluetoothState.Unknown or BluetoothState.TurningOn)
{
using var initializationTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, initializationTimeout.Token);
try
{
await stateChanged.Task.WaitAsync(linkedCancellation.Token);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
throw new InvalidOperationException(
"Bluetooth did not finish initializing. Try scanning again.");
}
}
}
finally
{
_bluetooth.StateChanged -= OnStateChanged;
}
}
if (_bluetooth.State == BluetoothState.On)
return;
throw _bluetooth.State switch
{
BluetoothState.Off or BluetoothState.TurningOff =>
new InvalidOperationException("Bluetooth is turned off."),
BluetoothState.Unauthorized =>
new PermissionException(
"Bluetooth access is denied. Enable Field Logger in System Settings > Privacy & Security > Bluetooth."),
BluetoothState.Unavailable =>
new InvalidOperationException("Bluetooth is not available on this Mac."),
_ => new InvalidOperationException($"Bluetooth is not ready (state: {_bluetooth.State}).")
};
}
/// <summary>Requests the runtime permissions BLE scanning needs (Android only; no-op elsewhere).</summary>
public static async Task EnsurePermissionsAsync()
{