using System.Globalization; namespace FieldLogger.Models; public enum GnssFixStatus { NoFix = 0, Single = 1, Dgps = 2, RtkFixed = 4, RtkFloat = 5 } /// /// A parsed $GNPOS sentence from the Maglink (H11) RTK receiver. /// 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; } = ""; /// Local UTC time this fix was received by the app. 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(), }; /// Parses a "$GNPOS,..." sentence (checksum must already be validated). 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); } /// /// A parsed $GNDEV device-information sentence from the Maglink receiver. /// 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], }; } } /// NMEA framing helpers ($...*hh checksum). public static class NmeaSentence { /// Returns the sentence body between '$' and '*' (or end of string). 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; } /// Validates the trailing *hh checksum. Sentences without a checksum fail validation. 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; } }