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:
155
FieldLogger/Models/GnssFix.cs
Normal file
155
FieldLogger/Models/GnssFix.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user