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:
98
FieldLogger/Services/MaglinkService.cs
Normal file
98
FieldLogger/Services/MaglinkService.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
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("0000fff2-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;
|
||||
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)
|
||||
{
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user