103 lines
3.9 KiB
C#
103 lines
3.9 KiB
C#
using FieldLogger.Models;
|
|
using FieldLogger.Services.Ble;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace FieldLogger.Services;
|
|
|
|
/// <summary>
|
|
/// Manages the connection to the Maglink (H11) RTK GNSS receiver: parses the
|
|
/// $GNPOS / $GNDEV custom NMEA stream and sends AT configuration commands.
|
|
/// </summary>
|
|
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<MaglinkService> _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<GnssFix>? FixReceived;
|
|
public event EventHandler<GnssDeviceInfo>? DeviceInfoReceived;
|
|
/// <summary>Every complete line received from the RTK receiver, before validation or parsing.</summary>
|
|
public event EventHandler<string>? LineReceived;
|
|
public event EventHandler? Disconnected;
|
|
|
|
/// <summary>Device-name filter for scan results (ML-*).</summary>
|
|
public static bool IsMaglinkName(string? name) =>
|
|
name is not null && name.StartsWith("ML-", StringComparison.OrdinalIgnoreCase);
|
|
|
|
public MaglinkService(ILogger<MaglinkService> 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);
|
|
}
|
|
|
|
/// <summary>Sends an AT command (terminator appended automatically).</summary>
|
|
public Task SendCommandAsync(string command, CancellationToken cancellationToken = default)
|
|
=> _client.WriteLineAsync(command, cancellationToken);
|
|
|
|
public Task DisconnectAsync() => _client.DisconnectAsync();
|
|
|
|
/// <summary>A fix received within the last few seconds, or null if the stream has gone stale.</summary>
|
|
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();
|
|
}
|