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>
156 lines
5.1 KiB
C#
156 lines
5.1 KiB
C#
using System.Globalization;
|
|
|
|
namespace FieldLogger.Models;
|
|
|
|
public enum GnssFixStatus { NoFix = 0, Single = 1, Dgps = 2, RtkFixed = 4, RtkFloat = 5 }
|
|
|
|
/// <summary>
|
|
/// A parsed $GNPOS sentence from the Maglink (H11) RTK receiver.
|
|
/// </summary>
|
|
public sealed class GnssFix
|
|
{
|
|
public double Latitude { get; init; }
|
|
public double Longitude { get; init; }
|
|
public double Altitude { get; init; }
|
|
public double AltitudeCorrected { get; init; }
|
|
public GnssFixStatus Status { get; init; }
|
|
public double Hdop { get; init; }
|
|
public double Hrms { get; init; }
|
|
public double Vrms { get; init; }
|
|
public int SatellitesUsed { get; init; }
|
|
public int SatellitesVisible { get; init; }
|
|
public double SpeedKmh { get; init; }
|
|
public double Heading { get; init; }
|
|
public double BatteryVoltage { get; init; }
|
|
public int BatteryPercent { get; init; }
|
|
public bool NtripConnected { get; init; }
|
|
public int RtcmSize { get; init; }
|
|
public double CorrectionAgeSeconds { get; init; }
|
|
public long UnixTimestamp { get; init; }
|
|
public double TiltAngle { get; init; }
|
|
public string RawSentence { get; init; } = "";
|
|
|
|
/// <summary>Local UTC time this fix was received by the app.</summary>
|
|
public DateTime ReceivedUtc { get; init; } = DateTime.UtcNow;
|
|
|
|
public bool HasPosition => Status != GnssFixStatus.NoFix;
|
|
|
|
public string StatusLabel => Status switch
|
|
{
|
|
GnssFixStatus.NoFix => "No Fix",
|
|
GnssFixStatus.Single => "Single",
|
|
GnssFixStatus.Dgps => "DGPS",
|
|
GnssFixStatus.RtkFixed => "RTK Fixed",
|
|
GnssFixStatus.RtkFloat => "RTK Float",
|
|
_ => Status.ToString(),
|
|
};
|
|
|
|
/// <summary>Parses a "$GNPOS,..." sentence (checksum must already be validated).</summary>
|
|
public static GnssFix? TryParse(string sentence)
|
|
{
|
|
var body = NmeaSentence.Body(sentence);
|
|
var f = body.Split(',');
|
|
if (f.Length < 20 || f[0] != "GNPOS")
|
|
return null;
|
|
|
|
try
|
|
{
|
|
return new GnssFix
|
|
{
|
|
Latitude = D(f[1]),
|
|
Longitude = D(f[2]),
|
|
Altitude = D(f[3]),
|
|
AltitudeCorrected = D(f[4]),
|
|
Status = (GnssFixStatus)I(f[5]),
|
|
Hdop = D(f[6]),
|
|
Hrms = D(f[7]),
|
|
Vrms = D(f[8]),
|
|
SatellitesUsed = I(f[9]),
|
|
SatellitesVisible = I(f[10]),
|
|
SpeedKmh = D(f[11]),
|
|
Heading = D(f[12]),
|
|
BatteryVoltage = D(f[13]),
|
|
BatteryPercent = I(f[14]),
|
|
NtripConnected = I(f[15]) != 0,
|
|
RtcmSize = I(f[16]),
|
|
CorrectionAgeSeconds = D(f[17]),
|
|
UnixTimestamp = long.Parse(f[18], CultureInfo.InvariantCulture),
|
|
TiltAngle = D(f[19]),
|
|
RawSentence = sentence.Trim(),
|
|
};
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static double D(string s) => double.Parse(s, NumberStyles.Float, CultureInfo.InvariantCulture);
|
|
private static int I(string s) => int.Parse(s, CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A parsed $GNDEV device-information sentence from the Maglink receiver.
|
|
/// </summary>
|
|
public sealed class GnssDeviceInfo
|
|
{
|
|
public string SerialNumber { get; init; } = "";
|
|
public string PcbVersion { get; init; } = "";
|
|
public string FirmwareVersion { get; init; } = "";
|
|
public string Imei { get; init; } = "";
|
|
public string Imsi { get; init; } = "";
|
|
public string Iccid { get; init; } = "";
|
|
|
|
public static GnssDeviceInfo? TryParse(string sentence)
|
|
{
|
|
var f = NmeaSentence.Body(sentence).Split(',');
|
|
if (f.Length < 7 || f[0] != "GNDEV")
|
|
return null;
|
|
|
|
return new GnssDeviceInfo
|
|
{
|
|
SerialNumber = f[1],
|
|
PcbVersion = f[2],
|
|
FirmwareVersion = f[3],
|
|
Imei = f[4],
|
|
Imsi = f[5],
|
|
Iccid = f[6],
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>NMEA framing helpers ($...*hh checksum).</summary>
|
|
public static class NmeaSentence
|
|
{
|
|
/// <summary>Returns the sentence body between '$' and '*' (or end of string).</summary>
|
|
public static string Body(string sentence)
|
|
{
|
|
var s = sentence.Trim().TrimStart('$');
|
|
var star = s.IndexOf('*');
|
|
return star >= 0 ? s[..star] : s;
|
|
}
|
|
|
|
public static byte Checksum(string sentence)
|
|
{
|
|
byte checksum = 0;
|
|
foreach (var c in Body(sentence))
|
|
checksum ^= (byte)c;
|
|
return checksum;
|
|
}
|
|
|
|
/// <summary>Validates the trailing *hh checksum. Sentences without a checksum fail validation.</summary>
|
|
public static bool VerifyChecksum(string sentence)
|
|
{
|
|
var star = sentence.LastIndexOf('*');
|
|
if (star < 0 || star + 3 > sentence.TrimEnd().Length)
|
|
return false;
|
|
|
|
var hex = sentence.Substring(star + 1, 2);
|
|
if (!byte.TryParse(hex, System.Globalization.NumberStyles.HexNumber,
|
|
System.Globalization.CultureInfo.InvariantCulture, out var expected))
|
|
return false;
|
|
|
|
return Checksum(sentence) == expected;
|
|
}
|
|
}
|