Initial commit: FieldLogger MAUI app with Maglink BLE support
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>
This commit is contained in:
175
FieldLogger/Services/DeviceConnectionManager.cs
Normal file
175
FieldLogger/Services/DeviceConnectionManager.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user