using FieldLogger.Models; using FieldLogger.Services.Ble; using Microsoft.Extensions.Logging; namespace FieldLogger.Services; /// /// Manages the connection to the Maglink (H11) RTK GNSS receiver: parses the /// $GNPOS / $GNDEV custom NMEA stream and sends AT configuration commands. /// public sealed class MaglinkService : IAsyncDisposable { public static readonly Guid ServiceUuid = Guid.Parse("0000fff0-0000-1000-8000-00805f9b34fb"); public static readonly Guid NotifyUuid = Guid.Parse("0000fff2-0000-1000-8000-00805f9b34fb"); public static readonly Guid WriteUuid = Guid.Parse("0000fff1-0000-1000-8000-00805f9b34fb"); private readonly BleSerialClient _client = new(); private readonly ILogger _logger; public GnssFix? LatestFix { get; private set; } public GnssDeviceInfo? DeviceInfo { get; private set; } public bool IsConnected => _client.IsConnected; public string? DeviceName => _client.DeviceName; public event EventHandler? FixReceived; public event EventHandler? DeviceInfoReceived; /// Every complete line received from the RTK receiver, before validation or parsing. public event EventHandler? LineReceived; public event EventHandler? Disconnected; /// Device-name filter for scan results (ML-*). public static bool IsMaglinkName(string? name) => name is not null && name.StartsWith("ML-", StringComparison.OrdinalIgnoreCase); public MaglinkService(ILogger logger) { _logger = logger; _client.LineReceived += OnLineReceived; _client.Disconnected += (_, _) => Disconnected?.Invoke(this, EventArgs.Empty); } public async Task ConnectAsync(Guid deviceId, CancellationToken cancellationToken = default) { await _client.ConnectAsync(deviceId, ServiceUuid, NotifyUuid, WriteUuid, cancellationToken); // Custom mode with GNPOS + GNDEV only - everything the app needs, minimal traffic. await SendCommandAsync("AT+BT_OUT=SET,1,0,1,1,0,0,0,0,0,0", cancellationToken); } /// Sends an AT command (terminator appended automatically). public Task SendCommandAsync(string command, CancellationToken cancellationToken = default) => _client.WriteLineAsync(command, cancellationToken); public Task DisconnectAsync() => _client.DisconnectAsync(); /// A fix received within the last few seconds, or null if the stream has gone stale. public GnssFix? FreshFix(TimeSpan? maxAge = null) { var fix = LatestFix; if (fix is null) return null; return DateTime.UtcNow - fix.ReceivedUtc <= (maxAge ?? TimeSpan.FromSeconds(5)) ? fix : null; } private void OnLineReceived(object? sender, string line) { LineReceived?.Invoke(this, line); if (line.StartsWith("$GNPOS,", StringComparison.Ordinal)) { if (!NmeaSentence.VerifyChecksum(line)) { _logger.LogWarning("GNPOS checksum failed: {Line}", line); return; } var fix = GnssFix.TryParse(line); if (fix is not null) { LatestFix = fix; FixReceived?.Invoke(this, fix); } } else if (line.StartsWith("$GNDEV,", StringComparison.Ordinal)) { if (!NmeaSentence.VerifyChecksum(line)) return; var info = GnssDeviceInfo.TryParse(line); if (info is not null) { DeviceInfo = info; DeviceInfoReceived?.Invoke(this, info); } } else { // AT command responses and anything else. _logger.LogDebug("Maglink rx: {Line}", line); } } public ValueTask DisposeAsync() => _client.DisposeAsync(); }