using CommunityToolkit.Mvvm.ComponentModel;
using FieldLogger.Models;
using FieldLogger.Services.Ble;
using Microsoft.Extensions.Logging;
namespace FieldLogger.Services;
public enum ConnectionState { Disconnected, Connecting, Connected }
///
/// Owns the lifetime of both device connections: auto-connects to saved devices
/// at startup, reconnects with backoff when a connection drops, and exposes
/// bindable state for the UI.
///
public sealed partial class DeviceConnectionManager : ObservableObject
{
private static readonly TimeSpan[] RetryDelays =
[TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)];
private readonly UmReceiverService _locator;
private readonly MaglinkService _gps;
private readonly SettingsService _settings;
private readonly ILogger _logger;
private CancellationTokenSource? _locatorCts;
private CancellationTokenSource? _gpsCts;
[ObservableProperty]
private ConnectionState _locatorState;
[ObservableProperty]
private ConnectionState _gpsState;
[ObservableProperty]
private string? _locatorName;
[ObservableProperty]
private string? _gpsName;
public UmReceiverService Locator => _locator;
public MaglinkService Gps => _gps;
public DeviceConnectionManager(UmReceiverService locator, MaglinkService gps,
SettingsService settings, ILogger logger)
{
_locator = locator;
_gps = gps;
_settings = settings;
_logger = logger;
_locator.Disconnected += (_, _) => OnDeviceDropped(DeviceKind.Locator);
_gps.Disconnected += (_, _) => OnDeviceDropped(DeviceKind.RtkGps);
LocatorName = settings.GetSavedDeviceName(DeviceKind.Locator);
GpsName = settings.GetSavedDeviceName(DeviceKind.RtkGps);
}
/// True when a device of this kind has been selected at least once.
public bool HasSavedDevice(DeviceKind kind) => _settings.GetSavedDeviceId(kind) is not null;
/// Kicks off auto-connect attempts for every saved device. Safe to call repeatedly.
public void Start()
{
if (HasSavedDevice(DeviceKind.Locator) && LocatorState == ConnectionState.Disconnected)
_ = ConnectLoopAsync(DeviceKind.Locator);
if (HasSavedDevice(DeviceKind.RtkGps) && GpsState == ConnectionState.Disconnected)
_ = ConnectLoopAsync(DeviceKind.RtkGps);
}
/// Saves a newly selected device and connects to it, replacing any previous device.
public async Task UseDeviceAsync(DeviceKind kind, Guid id, string name)
{
CancelLoop(kind);
await (kind == DeviceKind.Locator ? _locator.DisconnectAsync() : _gps.DisconnectAsync());
_settings.SaveDevice(kind, id, name);
if (kind == DeviceKind.Locator)
LocatorName = name;
else
GpsName = name;
_ = ConnectLoopAsync(kind);
}
public async Task ForgetDeviceAsync(DeviceKind kind)
{
CancelLoop(kind);
await (kind == DeviceKind.Locator ? _locator.DisconnectAsync() : _gps.DisconnectAsync());
_settings.ClearDevice(kind);
if (kind == DeviceKind.Locator)
{
LocatorName = null;
LocatorState = ConnectionState.Disconnected;
}
else
{
GpsName = null;
GpsState = ConnectionState.Disconnected;
}
}
private void OnDeviceDropped(DeviceKind kind)
{
_logger.LogInformation("{Kind} connection lost; scheduling reconnect", kind);
SetState(kind, ConnectionState.Disconnected);
_ = ConnectLoopAsync(kind);
}
private async Task ConnectLoopAsync(DeviceKind kind)
{
var id = _settings.GetSavedDeviceId(kind);
if (id is null)
return;
CancelLoop(kind);
var cts = new CancellationTokenSource();
if (kind == DeviceKind.Locator)
_locatorCts = cts;
else
_gpsCts = cts;
SetState(kind, ConnectionState.Connecting);
for (var attempt = 0; !cts.Token.IsCancellationRequested; attempt++)
{
try
{
await BleScanner.EnsurePermissionsAsync();
if (kind == DeviceKind.Locator)
await _locator.ConnectAsync(id.Value, cts.Token);
else
await _gps.ConnectAsync(id.Value, cts.Token);
SetState(kind, ConnectionState.Connected);
_logger.LogInformation("{Kind} connected", kind);
return;
}
catch (OperationCanceledException)
{
return;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "{Kind} connect attempt {Attempt} failed", kind, attempt + 1);
var delay = RetryDelays[Math.Min(attempt, RetryDelays.Length - 1)];
try { await Task.Delay(delay, cts.Token); }
catch (OperationCanceledException) { return; }
}
}
}
private void CancelLoop(DeviceKind kind)
{
var cts = kind == DeviceKind.Locator ? _locatorCts : _gpsCts;
cts?.Cancel();
cts?.Dispose();
if (kind == DeviceKind.Locator)
_locatorCts = null;
else
_gpsCts = null;
}
private void SetState(DeviceKind kind, ConnectionState state)
{
// Property change notifications must fire on the UI thread for bindings.
MainThread.BeginInvokeOnMainThread(() =>
{
if (kind == DeviceKind.Locator)
LocatorState = state;
else
GpsState = state;
});
}
}