Added Maglink RTK GNSS receiver integration with correct BLE UUIDs and device name filtering (ML-* prefix). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
176 lines
5.7 KiB
C#
176 lines
5.7 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using FieldLogger.Models;
|
|
using FieldLogger.Services.Ble;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace FieldLogger.Services;
|
|
|
|
public enum ConnectionState { Disconnected, Connecting, Connected }
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<DeviceConnectionManager> _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<DeviceConnectionManager> 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);
|
|
}
|
|
|
|
/// <summary>True when a device of this kind has been selected at least once.</summary>
|
|
public bool HasSavedDevice(DeviceKind kind) => _settings.GetSavedDeviceId(kind) is not null;
|
|
|
|
/// <summary>Kicks off auto-connect attempts for every saved device. Safe to call repeatedly.</summary>
|
|
public void Start()
|
|
{
|
|
if (HasSavedDevice(DeviceKind.Locator) && LocatorState == ConnectionState.Disconnected)
|
|
_ = ConnectLoopAsync(DeviceKind.Locator);
|
|
if (HasSavedDevice(DeviceKind.RtkGps) && GpsState == ConnectionState.Disconnected)
|
|
_ = ConnectLoopAsync(DeviceKind.RtkGps);
|
|
}
|
|
|
|
/// <summary>Saves a newly selected device and connects to it, replacing any previous device.</summary>
|
|
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;
|
|
});
|
|
}
|
|
}
|