From 6a60cebd519b9c66a0ee5cd9155cea9270f1b66e Mon Sep 17 00:00:00 2001 From: Joachim Spange Date: Tue, 28 Jan 2025 11:19:38 +0100 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=A8=20HDT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NmeaParser/Nmea/Hdt.cs | 64 +++++++++++++++++++ .../NmeaParser.Tests/NmeaMessages.cs | 11 ++++ 2 files changed, 75 insertions(+) create mode 100644 src/NmeaParser/Nmea/Hdt.cs diff --git a/src/NmeaParser/Nmea/Hdt.cs b/src/NmeaParser/Nmea/Hdt.cs new file mode 100644 index 0000000..8aebaf2 --- /dev/null +++ b/src/NmeaParser/Nmea/Hdt.cs @@ -0,0 +1,64 @@ +// ******************************************************************************* +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// ****************************************************************************** + +using System; +using System.Globalization; + +namespace NmeaParser.Messages +{ + /// + /// Heading from True North + /// + /// + /// + /// 1.: Heading in degrees + /// 2.: Indicates heading relative to True North + /// + /// + /// Actual vessel heading in degrees True produced by any device or system producing true heading + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "HeHdt")] + [NmeaMessageType("--HDT")] + public class Hdt : NmeaMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The message type + /// The NMEA message values. + public Hdt(string type, string[] message) : base(type, message) + { + if (message == null || message.Length < 2) + throw new ArgumentException("Invalid Hdt", "message"); + + // Extract the heading in degrees + HeadingInDeg = double.TryParse(message[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var heading) + ? heading + : double.NaN; + + HeadingRelToTrueNorth = message[1] == "T"; + } + + /// + /// Heading in degrees + /// + public double HeadingInDeg { get; } + + /// + /// T: Indicates heading relative to True North + /// + public bool HeadingRelToTrueNorth { get; } + } +} \ No newline at end of file diff --git a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs index b6cbc1c..1467fa6 100644 --- a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs +++ b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs @@ -296,6 +296,17 @@ namespace NmeaParser.Tests Assert.AreEqual(-1, gga.DgpsStationId); } + [TestMethod] + public void TestHEHDT() + { + const string input = "$HEHDT,13.37,T*29"; + var msg = NmeaMessage.Parse(input); + Assert.IsInstanceOfType(msg, typeof(Hdt)); + Hdt hdt = (Hdt)msg; + Assert.AreEqual(13.37, hdt.HeadingInDeg); + Assert.IsTrue(hdt.HeadingRelToTrueNorth); + } + [TestMethod] public void TestPtlna() { From b79651cc4baee734e5f7657ef3232d68012916a4 Mon Sep 17 00:00:00 2001 From: Joachim Spange Date: Tue, 28 Jan 2025 12:35:34 +0100 Subject: [PATCH 2/9] =?UTF-8?q?=E2=9C=A8=20DPT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NmeaParser/Nmea/Dpt.cs | 71 +++++++++++++++++++ .../NmeaParser.Tests/NmeaMessages.cs | 12 ++++ 2 files changed, 83 insertions(+) create mode 100644 src/NmeaParser/Nmea/Dpt.cs diff --git a/src/NmeaParser/Nmea/Dpt.cs b/src/NmeaParser/Nmea/Dpt.cs new file mode 100644 index 0000000..0028611 --- /dev/null +++ b/src/NmeaParser/Nmea/Dpt.cs @@ -0,0 +1,71 @@ +// ******************************************************************************* +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// ****************************************************************************** + +using System; +using System.Globalization; + +namespace NmeaParser.Messages +{ + /// + /// Water depth relative to the transducer, the depth offset of the transducer, and maximum depth that the sounder can detect a sea-bed (all in metres only). + /// Positive offsets provide distance from the transducer to the water line. Negative offsets provide distance from the transducer to the keel. + /// Not all NMEA 0183 devices that output this sentence can have their depth offset changed. In this case, the depth offset will always be zero, or not included. + /// NMEA 0183 v2.0 sentences will not include the maximum depth range value at all, as it was added in v3.0. + /// + /// + /// + /// Format: $xxDPT,DATA_METRES,OFFSET_METRES, MAXIMUM_METRES*hh + /// 1.: Data + /// 2.: Offset + /// 3.: Maximum + /// + /// + /// Depth + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "SdDpt")] + [NmeaMessageType("--DPT")] + public class Dpt : NmeaMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The message type + /// The NMEA message values. + public Dpt(string type, string[] message) : base(type, message) + { + if (message == null || message.Length < 3) + throw new ArgumentException("Invalid Dpt", "message"); + + DepthMeters = double.Parse(message[0], CultureInfo.InvariantCulture); + DepthOffsetMeters = double.Parse(message[1], CultureInfo.InvariantCulture); + MaxDepthRangeMeters = double.Parse(message[2], CultureInfo.InvariantCulture); + } + + /// + /// Depth, in meters + /// + public double DepthMeters { get; } + + /// + /// Depth offset, in meters + /// + public double DepthOffsetMeters { get; } + + /// + /// Maximum depth range, in meters + /// + public double MaxDepthRangeMeters { get; } + } +} \ No newline at end of file diff --git a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs index 1467fa6..3d58f9e 100644 --- a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs +++ b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs @@ -124,6 +124,18 @@ namespace NmeaParser.Tests Assert.ThrowsException(() => NmeaMessage.Parse(input, ignoreChecksum: false)); } + [TestMethod] + public void TestDPT() + { + string input = "$--DPT,149.5,000.5,1000.0*7F"; + var msg = NmeaMessage.Parse(input); + Assert.IsInstanceOfType(msg, typeof(Dpt)); + Dpt dpt = (Dpt)msg; + Assert.AreEqual(149.5, dpt.DepthMeters); + Assert.AreEqual(0.5, dpt.DepthOffsetMeters); + Assert.AreEqual(1000, dpt.MaxDepthRangeMeters); + } + [TestMethod] public void TestGprma() { From 128a8b16048b8acf2faec4b0274b23beb42d2973 Mon Sep 17 00:00:00 2001 From: Joachim Spange Date: Tue, 28 Jan 2025 15:18:25 +0100 Subject: [PATCH 3/9] =?UTF-8?q?=E2=9C=A8=20Support=20PTNL=20parent=20NMEA?= =?UTF-8?q?=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PTNL is parent to e.g. AVR, BPQ, DG, EVT, GGK, PJK, PJT, VGK, VHD --- src/NmeaParser/Nmea/NmeaMessage.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/NmeaParser/Nmea/NmeaMessage.cs b/src/NmeaParser/Nmea/NmeaMessage.cs index 0c474e1..6566bca 100644 --- a/src/NmeaParser/Nmea/NmeaMessage.cs +++ b/src/NmeaParser/Nmea/NmeaMessage.cs @@ -188,6 +188,11 @@ namespace NmeaParser.Messages string[] parts = message.Split(new char[] { ',' }); string MessageType = parts[0].Substring(1); + if (MessageType == "PTNL") { + // PTNL is parent to e.g. AVR, GGK etc. + MessageType = parts[1]; + parts = parts.Skip(1).ToArray(); + } if (MessageType == string.Empty) throw new ArgumentException("Missing NMEA Message Type"); string[] MessageParts = parts.Skip(1).ToArray(); From 647ff9fd0b6a908e123d41620aa72ddf406275a2 Mon Sep 17 00:00:00 2001 From: Joachim Spange Date: Wed, 29 Jan 2025 13:59:40 +0100 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9C=A8=20Parse=20date=20and=20time=20(mm?= =?UTF-8?q?ddyy=20and=20hhmmss.sss)=20format=20to=20UTC=20DateTime(fullYea?= =?UTF-8?q?r,=20month,=20day,=20hours,=20minutes,=20seconds,=20millisecond?= =?UTF-8?q?s,=20DateTimeKind.Utc)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NmeaParser/Nmea/NmeaMessage.cs | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/NmeaParser/Nmea/NmeaMessage.cs b/src/NmeaParser/Nmea/NmeaMessage.cs index 6566bca..a1cd9f2 100644 --- a/src/NmeaParser/Nmea/NmeaMessage.cs +++ b/src/NmeaParser/Nmea/NmeaMessage.cs @@ -309,6 +309,68 @@ namespace NmeaParser.Messages return TimeSpan.Zero; } + /// + /// Parse from mmddyy + hhmmss.ss to DateTime (used in e.g. Ggk) + /// https://receiverhelp.trimble.com/alloy-gnss/en-us/NMEA-0183messages_PTNL_GGK.html + /// + internal static DateTime? StringsToUtcDateTime(string dateField, string timeField) + { + if (string.IsNullOrWhiteSpace(dateField) || string.IsNullOrWhiteSpace(timeField)) + return null; + + // 2. Parse date (mmddyy) + // - Typically the year is 2 digits; you must decide how to handle the century. + // - For example, if year < 80 => 20xx; else => 19xx + if (dateField.Length < 6) return null; + if (!int.TryParse(dateField.Substring(0, 2), out int month)) return null; // mm + if (!int.TryParse(dateField.Substring(2, 2), out int day)) return null; // dd + if (!int.TryParse(dateField.Substring(4, 2), out int year)) return null; // yy + + int fullYear = (year < 80) ? (2000 + year) : (1900 + year); + + // 3. Parse time (hhmmss.ss) + // - The fractional seconds might be optional or might have variable length. + // - Parse hours, minutes, and then the part after the decimal. + // - Time can be "hhmmss", or "hhmmss.sss", etc. + // Example: "102939.00" => hh=10, mm=29, ss=39, fraction=0 + if (timeField.Length < 6) return null; + if (!int.TryParse(timeField.Substring(0, 2), out int hours)) return null; // hh + if (!int.TryParse(timeField.Substring(2, 2), out int minutes)) return null; // mm + + // The seconds part can have a decimal fraction + // So we split around the decimal point: + double secondsDouble; + if (timeField.Length > 4) + { + string secString = timeField.Substring(4); // "39.00 in example above" + if (!double.TryParse(secString, + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, + out secondsDouble)) + { + return null; + } + } + else + { + secondsDouble = 0.0; + } + int seconds = (int)secondsDouble; + int milliseconds = (int)((secondsDouble - seconds) * 1000.0); + + // 4. Construct the UTC DateTime + try + { + var result = new DateTime(fullYear, month, day, hours, minutes, seconds, milliseconds, DateTimeKind.Utc); + return result; + } + catch + { + // If the date/time is invalid (e.g. month=13, day=32), we return null or handle differently + return null; + } + } + /// /// Indicates whether the current object is equal to another object of the same type. /// From 40159323e94fede5f4e653a9703c36dfc6486b53 Mon Sep 17 00:00:00 2001 From: Joachim Spange Date: Wed, 29 Jan 2025 14:03:41 +0100 Subject: [PATCH 5/9] =?UTF-8?q?=E2=9C=A8=20PRNL,GGK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NmeaParser/Nmea/Ggk.cs | 165 ++++++++++++++++++ .../NmeaParser.Tests/NmeaMessages.cs | 17 ++ 2 files changed, 182 insertions(+) create mode 100644 src/NmeaParser/Nmea/Ggk.cs diff --git a/src/NmeaParser/Nmea/Ggk.cs b/src/NmeaParser/Nmea/Ggk.cs new file mode 100644 index 0000000..589e7b3 --- /dev/null +++ b/src/NmeaParser/Nmea/Ggk.cs @@ -0,0 +1,165 @@ +// ******************************************************************************* +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// ****************************************************************************** + +using System; +using System.Globalization; +using System.Linq; + +namespace NmeaParser.Messages +{ + /// + /// PTNL,GGK: Time, position, position type, DOP + /// + /// + /// + /// 1.: UTC time of position fix, in hhmmss.ss format. Hours must be two numbers, so may be padded. For example, 7 is shown as 07. + /// 2.: UTC date of position fix, in mmddyy format. Day must be two numbers, so may be padded. For example, 8 is shown as 08. + /// 3.: Latitude, in degrees and decimal minutes (dddmm.mmmmmmm) + /// 4.: Direction of latitude: N: North S: South + /// 5.: Longitude, in degrees and decimal minutes (dddmm.mmmmmmm). Should contain three digits of ddd. + /// 6.: Direction of longitude: E: East W: West + /// 7.: GPS Quality indicator: + /// 0: Fix not available or invalid + /// 1: Autonomous GPS fix + /// 2: RTK float solution + /// 3: RTK fix solution + /// 4: Differential, code phase only solution(DGPS) + /// 5: SBAS solution – WAAS/EGNOS/MSAS + /// 6: RTK float or RTK location 3D Network solution + /// 7: RTK fixed 3D Network solution + /// 8: RTK float or RTK location 2D in a Network solution + /// 9: RTK fixed 2D Network solution + /// 10: OmniSTAR HP / XP solution + /// 11: OmniSTAR VBS solution + /// 12: Location RTK solution + /// 13: Beacon DGPS + /// 14: CenterPoint RTX + /// 15: xFill + /// 8.: Number of satellites in fix + /// 9.: Dilution of Precision of fix (DOP) + /// 10.: Ellipsoidal height of fix (antenna height above ellipsoid). + /// 11.: M: ellipsoidal height is measured in meters + /// + /// + /// NOTE – The PTNL,GGK message is longer than the NMEA-0183 standard of 80 characters + /// NOTE – Even if a user-defined geoid model, or an inclined plane is loaded into the receiver, then the height output in the NMEA GGK string is always an ellipsoid height, for example, EHT24.123. + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ggk")] + [NmeaMessageType("GGK")] + public class Ggk : NmeaMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The message type + /// The NMEA message values. + public Ggk(string type, string[] message) : base(type, message) + { + if (message == null || message.Length < 11) + throw new ArgumentException("Invalid Ggk", "message"); + + UtcTime = StringsToUtcDateTime(message[1], message[0]); + Latitude = StringToLatitude(message[2], message[3]); + Longitude = StringToLongitude(message[4], message[5]); + if (!string.IsNullOrEmpty(message[6])) + Quality = (Ggk.QualityIndicator)int.Parse(message[6], CultureInfo.InvariantCulture); + if (!string.IsNullOrEmpty(message[7])) + NumberOfSatellites = int.Parse(message[7], CultureInfo.InvariantCulture); + DilutionOfPrecision = StringToDouble(message[8]); + EllipsoidalHeightOfFix = StringToDouble(message[9]); + EllipsoidalHeightIsMeasuredInMeters = message[10] == "M"; + } + + /// + /// UTC time of position fix, in DateTime format. + /// DateTime(fullYear, month, day, hours, minutes, seconds, milliseconds, DateTimeKind.Utc) + /// + public DateTime? UtcTime { get; } + + /// + /// Latitude, in degrees and decimal minutes (dddmm.mmmmmmm) + /// + public double Latitude { get; } + + /// + /// Longitude, in degrees and decimal minutes (dddmm.mmmmmmm) + /// + public double Longitude { get; } + + /// + /// GPS Quality indicator + /// + public Ggk.QualityIndicator Quality { get; } + + /// + /// Number of satellites in fix + /// + public int NumberOfSatellites { get; } + + /// + /// Dilution of Precision of fix (DOP) + /// + public double DilutionOfPrecision { get; } + + /// + /// Ellipsoidal height of fix (antenna height above ellipsoid). Must start with EHT. + /// + public double EllipsoidalHeightOfFix { get; } + + /// + /// M: ellipsoidal height is measured in meters + /// + public bool EllipsoidalHeightIsMeasuredInMeters { get; } + + /// + /// GPS Quality indicator + /// + public enum QualityIndicator : int + { + /// Fix not available or invalid + Invalid = 0, + /// Autonomous GPS fix + GpsFix = 1, + /// RTK float solution + RtkFloat = 2, + /// RTK fix solution + RtkFix = 3, + /// Differential, code phase only solution(DGPS) + Ggps = 4, + /// SBAS solution – WAAS/EGNOS/MSAS + Sbas = 5, + /// RTK float or RTK location 3D Network solution + RtkFloatOrLocation3DNetworkSolution = 6, + /// RTK fixed 3D Network solution + RtkFixed3DNetworkSolution = 7, + /// RTK float or RTK location 2D in a Network solution + RtkFloatOrLocation2DNetworkSolution = 8, + /// RTK fixed 2D Network solution + RtkFixed2DNetworkSolution = 9, + /// OmniSTAR HP / XP solution + OmistarHpXp = 10, + /// OmniSTAR VBS solution + OmniStarVbs = 11, + /// Location RTK solution + LocationRtk = 12, + /// Beacon DGPS + BeaconDgps = 13, + /// CenterPoint RTX + CenterPointRtx = 14, + /// xFill + XFill = 15, + } + } +} \ No newline at end of file diff --git a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs index 3d58f9e..a4d5d7b 100644 --- a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs +++ b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs @@ -337,6 +337,23 @@ namespace NmeaParser.Tests Assert.AreEqual('M', ptlna.SlopeDistanceUnits); } + [TestMethod] + public void TestPtnlGgk() + { + string input = "$PTNL,GGK,133703.14,012925,6104.64373420,N,01027.91199999,E,2,08,1.0,0.0,M,*0B"; + var msg = NmeaMessage.Parse(input); + Assert.IsInstanceOfType(msg, typeof(Ggk)); + Ggk ggk = (Ggk)msg; + Assert.AreEqual(new DateTime(2025, 1, 29, 13, 37, 3, 140, DateTimeKind.Utc), ggk.UtcTime); + Assert.AreEqual(61.07739557, ggk.Latitude); + Assert.AreEqual(10.465199999833333, ggk.Longitude); + Assert.AreEqual(Ggk.QualityIndicator.RtkFloat, ggk.Quality); + Assert.AreEqual(8, ggk.NumberOfSatellites); + Assert.AreEqual(1, ggk.DilutionOfPrecision); + Assert.AreEqual(0, ggk.EllipsoidalHeightOfFix); + Assert.IsTrue(ggk.EllipsoidalHeightIsMeasuredInMeters); + } + [TestMethod] public void TestPgrme() { From edb9f23464550e5de5b586a3a4da0b23ddf78a7e Mon Sep 17 00:00:00 2001 From: Joachim Spange Date: Thu, 30 Jan 2025 13:55:35 +0100 Subject: [PATCH 6/9] =?UTF-8?q?=E2=9C=85=20Skip=20checksum=20test=20for=20?= =?UTF-8?q?PTNL,GGK=20as=20it=20fails=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/UnitTests/NmeaParser.Tests/NmeaMessages.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs index a4d5d7b..446ab4a 100644 --- a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs +++ b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs @@ -85,6 +85,7 @@ namespace NmeaParser.Tests { var msg = NmeaMessage.Parse(line, previousMessage as IMultiSentenceMessage); Assert.IsNotNull(msg); + if (line.IndexOf("PTNL,") > 0) continue; // TODO PTNL var idx = line.IndexOf('*'); if (idx >= 0) { From 3678abc2c6e6846a6fc8f32a4765d5d8905389f7 Mon Sep 17 00:00:00 2001 From: Joachim Spange Date: Thu, 30 Jan 2025 14:16:55 +0100 Subject: [PATCH 7/9] =?UTF-8?q?=E2=9C=A8=20XDR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NmeaParser/Nmea/Xdr.cs | 71 +++++++++++++++++++ .../NmeaParser.Tests/NmeaMessages.cs | 13 ++++ 2 files changed, 84 insertions(+) create mode 100644 src/NmeaParser/Nmea/Xdr.cs diff --git a/src/NmeaParser/Nmea/Xdr.cs b/src/NmeaParser/Nmea/Xdr.cs new file mode 100644 index 0000000..c65c60c --- /dev/null +++ b/src/NmeaParser/Nmea/Xdr.cs @@ -0,0 +1,71 @@ +// ******************************************************************************* +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// ****************************************************************************** + +using System; + +namespace NmeaParser.Messages +{ + /// + /// Measurement data from transducers that measure physical quantities such as + /// temperature, force, pressure, frequency, angular or linear displacement, etc. + /// Four fields 'Type-Data-Units-ID' + /// + /// + /// + /// 1.: Transducer type + /// 2.: Measurement data + /// 3.: units of measure + /// 4.: transducer ID + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xdr")] + [NmeaMessageType("--XDR")] + public class Xdr : NmeaMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The message type + /// The NMEA message values. + public Xdr(string type, string[] message) : base(type, message) + { + if (message == null || message.Length < 4) + throw new ArgumentException("Invalid Xdr", "message"); + Type = message[0]; + Data = StringToDouble(message[1]); + Unit = message[2]; + ID = message[3]; + } + + /// + /// Transducer Type + /// + public string Type { get; } + + /// + /// Measurement data + /// + public double Data { get; } + + /// + /// Unit of measure + /// + public string Unit { get; } + + /// + /// ID + /// + public string ID { get; } + } +} \ No newline at end of file diff --git a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs index 446ab4a..8b9b4bf 100644 --- a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs +++ b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs @@ -320,6 +320,19 @@ namespace NmeaParser.Tests Assert.IsTrue(hdt.HeadingRelToTrueNorth); } + [TestMethod] + public void TestKMXDR() + { + string input = "$KMXDR,P,123.4,M,DEPTH*32"; + var msg = NmeaMessage.Parse(input); + Assert.IsInstanceOfType(msg, typeof(Xdr)); + Xdr xdr = (Xdr)msg; + Assert.AreEqual("P", xdr.Type); + Assert.AreEqual(123.4, xdr.Data); + Assert.AreEqual("M", xdr.Unit); + Assert.AreEqual("DEPTH", xdr.ID); + } + [TestMethod] public void TestPtlna() { From 531fe94d0dac964fc9da9ced45e502de66a30d9e Mon Sep 17 00:00:00 2001 From: Joachim Spange Date: Fri, 31 Jan 2025 14:13:45 +0100 Subject: [PATCH 8/9] =?UTF-8?q?=E2=9C=A8=20AML,SVM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NmeaParser/Nmea/NmeaMessage.cs | 3 +- src/NmeaParser/Nmea/Svm.cs | 76 +++++++++++++++++++ .../NmeaParser.Tests/NmeaMessages.cs | 15 ++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/NmeaParser/Nmea/Svm.cs diff --git a/src/NmeaParser/Nmea/NmeaMessage.cs b/src/NmeaParser/Nmea/NmeaMessage.cs index a1cd9f2..3ea186a 100644 --- a/src/NmeaParser/Nmea/NmeaMessage.cs +++ b/src/NmeaParser/Nmea/NmeaMessage.cs @@ -188,8 +188,9 @@ namespace NmeaParser.Messages string[] parts = message.Split(new char[] { ',' }); string MessageType = parts[0].Substring(1); - if (MessageType == "PTNL") { + if (MessageType is "PTNL" or "AML") { // PTNL is parent to e.g. AVR, GGK etc. + // AML is parent to e.g. SVT, SV, SVP etc MessageType = parts[1]; parts = parts.Skip(1).ToArray(); } diff --git a/src/NmeaParser/Nmea/Svm.cs b/src/NmeaParser/Nmea/Svm.cs new file mode 100644 index 0000000..57755a3 --- /dev/null +++ b/src/NmeaParser/Nmea/Svm.cs @@ -0,0 +1,76 @@ +// ******************************************************************************* +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// ****************************************************************************** + +using System; + +namespace NmeaParser.Messages +{ + /// + /// AML Sound Velocity (m/s), Temperature (C) + /// AML Oceanographic Svm sensor + /// + /// + /// + /// 1.: Sound Velocity m/s + /// 2.: Unit of temperature, TC = Celcius + /// 3.: Temperature + /// 4.: Label, SN: Serial number + /// 5.: Device serial number + /// + /// + [NmeaMessageType("SVM")] + public class Svm : NmeaMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The message type + /// The NMEA message values. + public Svm(string type, string[] message) : base(type, message) + { + if (message == null || message.Length < 5) + throw new ArgumentException("Invalid Svm", "message"); + SoundVelocity = StringToDouble(message[0]); + TemperatureUnit = message[1]; + Temperature = StringToDouble(message[2]); + IsSerialNumber = message[3] == "SN"; + SerialNumber = StringToDouble(message[4]); + } + + /// + /// Sound Velocity m/s + /// + public double SoundVelocity { get; } + + /// + /// Temperature Unit + /// + public string TemperatureUnit { get; } + + /// + /// Temperature + /// + public double Temperature { get; } + + /// + /// String indicating Serial number + /// + public bool IsSerialNumber { get; } + + /// + /// Serial Number + /// + public double SerialNumber { get; } + } +} \ No newline at end of file diff --git a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs index 8b9b4bf..7721b0c 100644 --- a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs +++ b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs @@ -86,6 +86,7 @@ namespace NmeaParser.Tests var msg = NmeaMessage.Parse(line, previousMessage as IMultiSentenceMessage); Assert.IsNotNull(msg); if (line.IndexOf("PTNL,") > 0) continue; // TODO PTNL + if (line.IndexOf("AML,") > 0) continue; // TODO AML var idx = line.IndexOf('*'); if (idx >= 0) { @@ -125,6 +126,20 @@ namespace NmeaParser.Tests Assert.ThrowsException(() => NmeaMessage.Parse(input, ignoreChecksum: false)); } + [TestMethod] + public void TestAmlSvm() + { + string input = "$AML,SVM,1468.951,TC,15.753,SN,168753*0F"; + var msg = NmeaMessage.Parse(input); + Assert.IsInstanceOfType(msg, typeof(Svm)); + Svm svm = (Svm)msg; + Assert.AreEqual(1468.951, svm.SoundVelocity); + Assert.AreEqual("TC", svm.TemperatureUnit); + Assert.AreEqual(15.753, svm.Temperature); + Assert.IsTrue(svm.IsSerialNumber); + Assert.AreEqual(168753, svm.SerialNumber); + } + [TestMethod] public void TestDPT() { From eb9904e1df9ac59be7ee383d3740acd1222d4ded Mon Sep 17 00:00:00 2001 From: Joachim Spange Date: Tue, 4 Feb 2025 14:11:35 +0100 Subject: [PATCH 9/9] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20evert=20PTNL,GGK=20and?= =?UTF-8?q?=20AML,SVM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NmeaParser/Nmea/Ggk.cs | 165 ------------------ src/NmeaParser/Nmea/NmeaMessage.cs | 68 -------- src/NmeaParser/Nmea/Svm.cs | 76 -------- .../NmeaParser.Tests/NmeaMessages.cs | 33 ---- 4 files changed, 342 deletions(-) delete mode 100644 src/NmeaParser/Nmea/Ggk.cs delete mode 100644 src/NmeaParser/Nmea/Svm.cs diff --git a/src/NmeaParser/Nmea/Ggk.cs b/src/NmeaParser/Nmea/Ggk.cs deleted file mode 100644 index 589e7b3..0000000 --- a/src/NmeaParser/Nmea/Ggk.cs +++ /dev/null @@ -1,165 +0,0 @@ -// ******************************************************************************* -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// ****************************************************************************** - -using System; -using System.Globalization; -using System.Linq; - -namespace NmeaParser.Messages -{ - /// - /// PTNL,GGK: Time, position, position type, DOP - /// - /// - /// - /// 1.: UTC time of position fix, in hhmmss.ss format. Hours must be two numbers, so may be padded. For example, 7 is shown as 07. - /// 2.: UTC date of position fix, in mmddyy format. Day must be two numbers, so may be padded. For example, 8 is shown as 08. - /// 3.: Latitude, in degrees and decimal minutes (dddmm.mmmmmmm) - /// 4.: Direction of latitude: N: North S: South - /// 5.: Longitude, in degrees and decimal minutes (dddmm.mmmmmmm). Should contain three digits of ddd. - /// 6.: Direction of longitude: E: East W: West - /// 7.: GPS Quality indicator: - /// 0: Fix not available or invalid - /// 1: Autonomous GPS fix - /// 2: RTK float solution - /// 3: RTK fix solution - /// 4: Differential, code phase only solution(DGPS) - /// 5: SBAS solution – WAAS/EGNOS/MSAS - /// 6: RTK float or RTK location 3D Network solution - /// 7: RTK fixed 3D Network solution - /// 8: RTK float or RTK location 2D in a Network solution - /// 9: RTK fixed 2D Network solution - /// 10: OmniSTAR HP / XP solution - /// 11: OmniSTAR VBS solution - /// 12: Location RTK solution - /// 13: Beacon DGPS - /// 14: CenterPoint RTX - /// 15: xFill - /// 8.: Number of satellites in fix - /// 9.: Dilution of Precision of fix (DOP) - /// 10.: Ellipsoidal height of fix (antenna height above ellipsoid). - /// 11.: M: ellipsoidal height is measured in meters - /// - /// - /// NOTE – The PTNL,GGK message is longer than the NMEA-0183 standard of 80 characters - /// NOTE – Even if a user-defined geoid model, or an inclined plane is loaded into the receiver, then the height output in the NMEA GGK string is always an ellipsoid height, for example, EHT24.123. - /// - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ggk")] - [NmeaMessageType("GGK")] - public class Ggk : NmeaMessage - { - /// - /// Initializes a new instance of the class. - /// - /// The message type - /// The NMEA message values. - public Ggk(string type, string[] message) : base(type, message) - { - if (message == null || message.Length < 11) - throw new ArgumentException("Invalid Ggk", "message"); - - UtcTime = StringsToUtcDateTime(message[1], message[0]); - Latitude = StringToLatitude(message[2], message[3]); - Longitude = StringToLongitude(message[4], message[5]); - if (!string.IsNullOrEmpty(message[6])) - Quality = (Ggk.QualityIndicator)int.Parse(message[6], CultureInfo.InvariantCulture); - if (!string.IsNullOrEmpty(message[7])) - NumberOfSatellites = int.Parse(message[7], CultureInfo.InvariantCulture); - DilutionOfPrecision = StringToDouble(message[8]); - EllipsoidalHeightOfFix = StringToDouble(message[9]); - EllipsoidalHeightIsMeasuredInMeters = message[10] == "M"; - } - - /// - /// UTC time of position fix, in DateTime format. - /// DateTime(fullYear, month, day, hours, minutes, seconds, milliseconds, DateTimeKind.Utc) - /// - public DateTime? UtcTime { get; } - - /// - /// Latitude, in degrees and decimal minutes (dddmm.mmmmmmm) - /// - public double Latitude { get; } - - /// - /// Longitude, in degrees and decimal minutes (dddmm.mmmmmmm) - /// - public double Longitude { get; } - - /// - /// GPS Quality indicator - /// - public Ggk.QualityIndicator Quality { get; } - - /// - /// Number of satellites in fix - /// - public int NumberOfSatellites { get; } - - /// - /// Dilution of Precision of fix (DOP) - /// - public double DilutionOfPrecision { get; } - - /// - /// Ellipsoidal height of fix (antenna height above ellipsoid). Must start with EHT. - /// - public double EllipsoidalHeightOfFix { get; } - - /// - /// M: ellipsoidal height is measured in meters - /// - public bool EllipsoidalHeightIsMeasuredInMeters { get; } - - /// - /// GPS Quality indicator - /// - public enum QualityIndicator : int - { - /// Fix not available or invalid - Invalid = 0, - /// Autonomous GPS fix - GpsFix = 1, - /// RTK float solution - RtkFloat = 2, - /// RTK fix solution - RtkFix = 3, - /// Differential, code phase only solution(DGPS) - Ggps = 4, - /// SBAS solution – WAAS/EGNOS/MSAS - Sbas = 5, - /// RTK float or RTK location 3D Network solution - RtkFloatOrLocation3DNetworkSolution = 6, - /// RTK fixed 3D Network solution - RtkFixed3DNetworkSolution = 7, - /// RTK float or RTK location 2D in a Network solution - RtkFloatOrLocation2DNetworkSolution = 8, - /// RTK fixed 2D Network solution - RtkFixed2DNetworkSolution = 9, - /// OmniSTAR HP / XP solution - OmistarHpXp = 10, - /// OmniSTAR VBS solution - OmniStarVbs = 11, - /// Location RTK solution - LocationRtk = 12, - /// Beacon DGPS - BeaconDgps = 13, - /// CenterPoint RTX - CenterPointRtx = 14, - /// xFill - XFill = 15, - } - } -} \ No newline at end of file diff --git a/src/NmeaParser/Nmea/NmeaMessage.cs b/src/NmeaParser/Nmea/NmeaMessage.cs index 3ea186a..0c474e1 100644 --- a/src/NmeaParser/Nmea/NmeaMessage.cs +++ b/src/NmeaParser/Nmea/NmeaMessage.cs @@ -188,12 +188,6 @@ namespace NmeaParser.Messages string[] parts = message.Split(new char[] { ',' }); string MessageType = parts[0].Substring(1); - if (MessageType is "PTNL" or "AML") { - // PTNL is parent to e.g. AVR, GGK etc. - // AML is parent to e.g. SVT, SV, SVP etc - MessageType = parts[1]; - parts = parts.Skip(1).ToArray(); - } if (MessageType == string.Empty) throw new ArgumentException("Missing NMEA Message Type"); string[] MessageParts = parts.Skip(1).ToArray(); @@ -310,68 +304,6 @@ namespace NmeaParser.Messages return TimeSpan.Zero; } - /// - /// Parse from mmddyy + hhmmss.ss to DateTime (used in e.g. Ggk) - /// https://receiverhelp.trimble.com/alloy-gnss/en-us/NMEA-0183messages_PTNL_GGK.html - /// - internal static DateTime? StringsToUtcDateTime(string dateField, string timeField) - { - if (string.IsNullOrWhiteSpace(dateField) || string.IsNullOrWhiteSpace(timeField)) - return null; - - // 2. Parse date (mmddyy) - // - Typically the year is 2 digits; you must decide how to handle the century. - // - For example, if year < 80 => 20xx; else => 19xx - if (dateField.Length < 6) return null; - if (!int.TryParse(dateField.Substring(0, 2), out int month)) return null; // mm - if (!int.TryParse(dateField.Substring(2, 2), out int day)) return null; // dd - if (!int.TryParse(dateField.Substring(4, 2), out int year)) return null; // yy - - int fullYear = (year < 80) ? (2000 + year) : (1900 + year); - - // 3. Parse time (hhmmss.ss) - // - The fractional seconds might be optional or might have variable length. - // - Parse hours, minutes, and then the part after the decimal. - // - Time can be "hhmmss", or "hhmmss.sss", etc. - // Example: "102939.00" => hh=10, mm=29, ss=39, fraction=0 - if (timeField.Length < 6) return null; - if (!int.TryParse(timeField.Substring(0, 2), out int hours)) return null; // hh - if (!int.TryParse(timeField.Substring(2, 2), out int minutes)) return null; // mm - - // The seconds part can have a decimal fraction - // So we split around the decimal point: - double secondsDouble; - if (timeField.Length > 4) - { - string secString = timeField.Substring(4); // "39.00 in example above" - if (!double.TryParse(secString, - System.Globalization.NumberStyles.Float, - System.Globalization.CultureInfo.InvariantCulture, - out secondsDouble)) - { - return null; - } - } - else - { - secondsDouble = 0.0; - } - int seconds = (int)secondsDouble; - int milliseconds = (int)((secondsDouble - seconds) * 1000.0); - - // 4. Construct the UTC DateTime - try - { - var result = new DateTime(fullYear, month, day, hours, minutes, seconds, milliseconds, DateTimeKind.Utc); - return result; - } - catch - { - // If the date/time is invalid (e.g. month=13, day=32), we return null or handle differently - return null; - } - } - /// /// Indicates whether the current object is equal to another object of the same type. /// diff --git a/src/NmeaParser/Nmea/Svm.cs b/src/NmeaParser/Nmea/Svm.cs deleted file mode 100644 index 57755a3..0000000 --- a/src/NmeaParser/Nmea/Svm.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ******************************************************************************* -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// ****************************************************************************** - -using System; - -namespace NmeaParser.Messages -{ - /// - /// AML Sound Velocity (m/s), Temperature (C) - /// AML Oceanographic Svm sensor - /// - /// - /// - /// 1.: Sound Velocity m/s - /// 2.: Unit of temperature, TC = Celcius - /// 3.: Temperature - /// 4.: Label, SN: Serial number - /// 5.: Device serial number - /// - /// - [NmeaMessageType("SVM")] - public class Svm : NmeaMessage - { - /// - /// Initializes a new instance of the class. - /// - /// The message type - /// The NMEA message values. - public Svm(string type, string[] message) : base(type, message) - { - if (message == null || message.Length < 5) - throw new ArgumentException("Invalid Svm", "message"); - SoundVelocity = StringToDouble(message[0]); - TemperatureUnit = message[1]; - Temperature = StringToDouble(message[2]); - IsSerialNumber = message[3] == "SN"; - SerialNumber = StringToDouble(message[4]); - } - - /// - /// Sound Velocity m/s - /// - public double SoundVelocity { get; } - - /// - /// Temperature Unit - /// - public string TemperatureUnit { get; } - - /// - /// Temperature - /// - public double Temperature { get; } - - /// - /// String indicating Serial number - /// - public bool IsSerialNumber { get; } - - /// - /// Serial Number - /// - public double SerialNumber { get; } - } -} \ No newline at end of file diff --git a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs index 7721b0c..50ed8cd 100644 --- a/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs +++ b/src/UnitTests/NmeaParser.Tests/NmeaMessages.cs @@ -85,8 +85,6 @@ namespace NmeaParser.Tests { var msg = NmeaMessage.Parse(line, previousMessage as IMultiSentenceMessage); Assert.IsNotNull(msg); - if (line.IndexOf("PTNL,") > 0) continue; // TODO PTNL - if (line.IndexOf("AML,") > 0) continue; // TODO AML var idx = line.IndexOf('*'); if (idx >= 0) { @@ -126,20 +124,6 @@ namespace NmeaParser.Tests Assert.ThrowsException(() => NmeaMessage.Parse(input, ignoreChecksum: false)); } - [TestMethod] - public void TestAmlSvm() - { - string input = "$AML,SVM,1468.951,TC,15.753,SN,168753*0F"; - var msg = NmeaMessage.Parse(input); - Assert.IsInstanceOfType(msg, typeof(Svm)); - Svm svm = (Svm)msg; - Assert.AreEqual(1468.951, svm.SoundVelocity); - Assert.AreEqual("TC", svm.TemperatureUnit); - Assert.AreEqual(15.753, svm.Temperature); - Assert.IsTrue(svm.IsSerialNumber); - Assert.AreEqual(168753, svm.SerialNumber); - } - [TestMethod] public void TestDPT() { @@ -366,23 +350,6 @@ namespace NmeaParser.Tests Assert.AreEqual('M', ptlna.SlopeDistanceUnits); } - [TestMethod] - public void TestPtnlGgk() - { - string input = "$PTNL,GGK,133703.14,012925,6104.64373420,N,01027.91199999,E,2,08,1.0,0.0,M,*0B"; - var msg = NmeaMessage.Parse(input); - Assert.IsInstanceOfType(msg, typeof(Ggk)); - Ggk ggk = (Ggk)msg; - Assert.AreEqual(new DateTime(2025, 1, 29, 13, 37, 3, 140, DateTimeKind.Utc), ggk.UtcTime); - Assert.AreEqual(61.07739557, ggk.Latitude); - Assert.AreEqual(10.465199999833333, ggk.Longitude); - Assert.AreEqual(Ggk.QualityIndicator.RtkFloat, ggk.Quality); - Assert.AreEqual(8, ggk.NumberOfSatellites); - Assert.AreEqual(1, ggk.DilutionOfPrecision); - Assert.AreEqual(0, ggk.EllipsoidalHeightOfFix); - Assert.IsTrue(ggk.EllipsoidalHeightIsMeasuredInMeters); - } - [TestMethod] public void TestPgrme() {