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;
}
}