From 826d062569926452d63da41582cb51f69d0ff7e7 Mon Sep 17 00:00:00 2001 From: Morten Nielsen <1378165+dotMorten@users.noreply.github.com> Date: Wed, 22 Jul 2020 08:42:40 -0700 Subject: [PATCH 01/62] Update ArcGISRuntime.md --- docs/concepts/ArcGISRuntime.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/concepts/ArcGISRuntime.md b/docs/concepts/ArcGISRuntime.md index d848227..3fed588 100644 --- a/docs/concepts/ArcGISRuntime.md +++ b/docs/concepts/ArcGISRuntime.md @@ -39,8 +39,8 @@ namespace NmeaParser.ArcGIS { LocationChanged?.Invoke(this, new LocationInfo() { - Course = rmc.Course, - Speed = rmc.Speed, + Course = double.IsNaN(rmc.Course) ? 0 : rmc.Course, // Current ArcGIS Runtime limitation that course can't be NaN + Speed = double.IsNaN(rmc.Speed) ? 0 : rmc.Speed, Location = new MapPoint(rmc.Longitude, rmc.Latitude, SpatialReferences.Wgs84) }); } From 168b2d0040b205380548e1fe56305644a197b691 Mon Sep 17 00:00:00 2001 From: Morten Nielsen <1378165+dotMorten@users.noreply.github.com> Date: Wed, 22 Jul 2020 09:05:42 -0700 Subject: [PATCH 02/62] Updated sample to 100.x design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Can't believe I didn't notice this was still using 10.x until now 🤦‍♂️ --- docs/concepts/ArcGISRuntime.md | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/concepts/ArcGISRuntime.md b/docs/concepts/ArcGISRuntime.md index 3fed588..f183df9 100644 --- a/docs/concepts/ArcGISRuntime.md +++ b/docs/concepts/ArcGISRuntime.md @@ -8,47 +8,46 @@ You can also check out the Desktop Sample app in the [Github Repo]( https://gith **Usage:** ```csharp NmeaParser.NmeaDevice device = new NmeaParser.NmeaFileDevice("NmeaSampleData.txt"); -mapView.LocationDisplay.LocationProvider = new NmeaLocationProvider(device); +mapView.LocationDisplay.DataSource = new NmeaLocationProvider(device); +mapView.LocationDisplay.InitialZoomScale = 20000; mapView.LocationDisplay.IsEnabled = true; ``` **NmeaLocationProvider.cs** ```csharp -using System; using System.Threading.Tasks; using Esri.ArcGISRuntime.Geometry; using Esri.ArcGISRuntime.Location; +using NmeaParser; namespace NmeaParser.ArcGIS { - public class NmeaLocationProvider : ILocationProvider + public class NmeaLocationProvider : LocationDataSource { - public event EventHandler LocationChanged; private readonly NmeaParser.NmeaDevice device; public NmeaLocationProvider(NmeaParser.NmeaDevice device) { this.device = device; - device.MessageReceived += device_MessageReceived; + device.MessageReceived += NmeaMessageReceived; } - void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs e) + private void NmeaMessageReceived(object sender, NmeaMessageReceivedEventArgs e) { var message = e.Message; if (message is NmeaParser.Messages.Rmc rmc && rmc.Active) { - LocationChanged?.Invoke(this, new LocationInfo() - { - Course = double.IsNaN(rmc.Course) ? 0 : rmc.Course, // Current ArcGIS Runtime limitation that course can't be NaN - Speed = double.IsNaN(rmc.Speed) ? 0 : rmc.Speed, - Location = new MapPoint(rmc.Longitude, rmc.Latitude, SpatialReferences.Wgs84) - }); + base.UpdateLocation(new Location( + new MapPoint(rmc.Longitude, rmc.Latitude, SpatialReferences.Wgs84), + horizontalAccuracy: double.NaN, + velocity: double.IsNaN(rmc.Speed) ? 0 : rmc.Speed, + course: double.IsNaN(rmc.Course) ? 0 : rmc.Course, // Current ArcGIS Runtime limitation that course can't be NaN + isLastKnown: false)); } } + protected override Task OnStartAsync() => device.OpenAsync(); - public Task StartAsync() => device.OpenAsync(); - - public Task StopAsync() => device.CloseAsync(); + protected override Task OnStopAsync() => device.CloseAsync(); } } From adef7b83c44a47f18c5e58afc4145b1971f1f9e2 Mon Sep 17 00:00:00 2001 From: Morten Nielsen <1378165+dotMorten@users.noreply.github.com> Date: Wed, 22 Jul 2020 10:37:52 -0700 Subject: [PATCH 03/62] Added a more advanced example with combined messages --- docs/concepts/ArcGISRuntime.md | 136 +++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/docs/concepts/ArcGISRuntime.md b/docs/concepts/ArcGISRuntime.md index f183df9..74d6ef6 100644 --- a/docs/concepts/ArcGISRuntime.md +++ b/docs/concepts/ArcGISRuntime.md @@ -53,4 +53,140 @@ namespace NmeaParser.ArcGIS ``` +### Combining multiple NMEA messages into a single location event +NMEA often happens in a burst of messages, which could be combined to one larger location object with more information available. +By relying on the time stamp in most of the messages, we can combine them all to get better metadata about the location. +``` +using System; +using System.Linq; +using System.Threading.Tasks; +using Esri.ArcGISRuntime.Geometry; +using Esri.ArcGISRuntime.Location; +using NmeaParser; +using NmeaParser.Messages; + +namespace NmeaParser.ArcGIS +{ + public class NmeaLocationProvider : LocationDataSource + { + private readonly NmeaParser.NmeaDevice device; + private Gga lastGga; + private Rmc lastRmc; + private Gsa lastGsa; + private Gst lastGst; + + public NmeaLocationProvider(NmeaParser.NmeaDevice device) + { + this.device = device; + device.MessageReceived += NmeaMessageReceived; + } + private void NmeaMessageReceived(object sender, NmeaMessageReceivedEventArgs e) + { + var message = e.Message; + bool newFix = false; + if (message is Rmc rmc && rmc.Active) + { + lastRmc = rmc; + newFix = true; + } + else if (message is Gga gga) + { + lastGga = gga; + newFix = true; + } + else if (message is Gst gst) + { + lastGst = gst; + newFix = true; + } + else if (message is Gsa gsa) + { + lastGsa = gsa; + } + else + { + return; + } + // We require the timestamps to match to raise them together. Gsa doesn't have a time stamp so just using latest for that + TimeSpan? timeOfFixMax = MaxTime(lastRmc?.FixTime.TimeOfDay, lastGga?.FixTime, lastGst?.FixTime); + TimeSpan? timeOfFixMin = MinTime(lastRmc?.FixTime.TimeOfDay, lastGga?.FixTime, lastGst?.FixTime); + if (newFix && timeOfFixMax == timeOfFixMin) + { + var location = NmeaLocation.Create(timeOfFixMax.Value, lastRmc, lastGga, lastGsa, lastGst); + if (location != null) + base.UpdateLocation(location); + } + } + private static TimeSpan? MaxTime(params TimeSpan?[] timeSpans) => timeSpans.Where(t => t != null).Max(); + private static TimeSpan? MinTime(params TimeSpan?[] timeSpans) => timeSpans.Where(t => t != null).Min(); + protected override Task OnStartAsync() => device.OpenAsync(); + + protected override Task OnStopAsync() => device.CloseAsync(); + } + + /// + /// Custom location class with the additional NMEA information associated with it + /// + public class NmeaLocation : Location + { + private NmeaLocation(DateTimeOffset? timestamp, MapPoint position, double horizontalAccuracy, double verticalAccuracy, double velocity, double course, bool isLastKnown) + : base(timestamp, position, horizontalAccuracy, verticalAccuracy, velocity, course, isLastKnown) + { + } + + public static NmeaLocation Create(TimeSpan timeOfFix, Rmc rmc, Gga gga, Gsa gsa, Gst gst) + { + MapPoint position = null; + double horizontalAccuracy = double.NaN; + double verticalAccuracy = double.NaN; + double velocity = 0; + double course = 0; + // Prefer GGA over RMC for location + if (gga != null && gga.FixTime == timeOfFix) + { + if (double.IsNaN(gga.Altitude)) + position = new MapPoint(gga.Longitude, gga.Latitude, SpatialReferences.Wgs84); + else + { + // Vertical id 115700 == ellipsoid reference system. Gga is geoid, but we subtract GeoidalSeparation to simplify + // vertical transformations from the simpler/better known ellipsoidal model + position = new MapPoint(gga.Longitude, gga.Latitude, gga.Altitude - gga.GeoidalSeparation, SpatialReference.Create(4326, 115700)); + } + } + if (rmc != null && rmc.FixTime.TimeOfDay == timeOfFix) + { + if (position == null) + { + position = new MapPoint(rmc.Longitude, rmc.Latitude, SpatialReferences.Wgs84); + } + velocity = double.IsNaN(rmc.Speed) ? 0 : rmc.Speed; + course = double.IsNaN(rmc.Course) ? 0 : rmc.Course; + } + if (gst != null && gst.FixTime == timeOfFix) + { + verticalAccuracy = gst.SigmaHeightError; + horizontalAccuracy = gst.SemiMajorError; + } + if (position == null) + return null; + var location = new NmeaLocation(DateTimeOffset.UtcNow.Date.Add(timeOfFix), position, horizontalAccuracy, verticalAccuracy, velocity, course, false); + location.Rmc = rmc; + location.Gga = gga; + location.Gsa = gsa; + location.Gst = gst?.FixTime == timeOfFix ? gst : null; + return location; + } + public Rmc Rmc { get; private set; } + public Gga Gga { get; private set; } + public Gsa Gsa { get; private set; } + public Gst Gst { get; private set; } + + public int NumberOfSatellites => Gga?.NumberOfSatellites ?? -1; + public double Hdop => Gsa?.Hdop ?? Gga?.Hdop ?? double.NaN; + public double Pdop => Gsa?.Pdop ?? double.NaN; + public double Vdop => Gsa?.Vdop ?? double.NaN; + } +} +``` + ![Screenshot](https://user-images.githubusercontent.com/1378165/73328707-95990e80-420f-11ea-85a7-43149e29bd21.png) From 2b4462f18764aaf84cf0693930b05d14800f8868 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Wed, 22 Jul 2020 10:50:21 -0700 Subject: [PATCH 04/62] Added ITimestampedMessage Added ITimestampedMessage interface to easily get time stamps from any message that supports it --- src/NmeaParser/Nmea/Gga.cs | 4 +++- src/NmeaParser/Nmea/Gll.cs | 4 +++- src/NmeaParser/Nmea/Gns.cs | 4 +++- src/NmeaParser/Nmea/Gst.cs | 4 +++- src/NmeaParser/Nmea/ITimestampedMessage.cs | 17 +++++++++++++++++ src/NmeaParser/Nmea/Rmc.cs | 4 +++- src/NmeaParser/Nmea/Zda.cs | 4 +++- src/NmeaParser/NmeaParser.csproj | 5 +++-- 8 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 src/NmeaParser/Nmea/ITimestampedMessage.cs diff --git a/src/NmeaParser/Nmea/Gga.cs b/src/NmeaParser/Nmea/Gga.cs index bd75c56..831737e 100644 --- a/src/NmeaParser/Nmea/Gga.cs +++ b/src/NmeaParser/Nmea/Gga.cs @@ -26,7 +26,7 @@ namespace NmeaParser.Messages /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gpgga")] [NmeaMessageType("--GGA")] - public class Gga : NmeaMessage + public class Gga : NmeaMessage, ITimestampedMessage { /// /// Initializes a new instance of the class. @@ -123,6 +123,8 @@ namespace NmeaParser.Messages /// public int DgpsStationId { get; } + TimeSpan ITimestampedMessage.Timestamp => FixTime; + /// /// Fix quality indicater /// diff --git a/src/NmeaParser/Nmea/Gll.cs b/src/NmeaParser/Nmea/Gll.cs index 625d0dd..52fe6e1 100644 --- a/src/NmeaParser/Nmea/Gll.cs +++ b/src/NmeaParser/Nmea/Gll.cs @@ -24,7 +24,7 @@ namespace NmeaParser.Messages /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gll")] [NmeaMessageType("--GLL")] - public class Gll : NmeaMessage + public class Gll : NmeaMessage, ITimestampedMessage { /// /// Initializes a new instance of the class. @@ -85,6 +85,8 @@ namespace NmeaParser.Messages /// public Mode ModeIndicator { get; } + TimeSpan ITimestampedMessage.Timestamp => FixTime; + /// /// Positioning system Mode Indicator /// diff --git a/src/NmeaParser/Nmea/Gns.cs b/src/NmeaParser/Nmea/Gns.cs index 4afc73e..a2183eb 100644 --- a/src/NmeaParser/Nmea/Gns.cs +++ b/src/NmeaParser/Nmea/Gns.cs @@ -38,7 +38,7 @@ namespace NmeaParser.Messages /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gns")] [NmeaMessageType("--GNS")] - public class Gns : NmeaMessage + public class Gns : NmeaMessage, ITimestampedMessage { /* * Example of GNS messages: @@ -258,5 +258,7 @@ namespace NmeaParser.Messages /// Navigational status /// public NavigationalStatus Status { get; } + + TimeSpan ITimestampedMessage.Timestamp => FixTime; } } diff --git a/src/NmeaParser/Nmea/Gst.cs b/src/NmeaParser/Nmea/Gst.cs index bf0f7a1..f3e09e0 100644 --- a/src/NmeaParser/Nmea/Gst.cs +++ b/src/NmeaParser/Nmea/Gst.cs @@ -21,7 +21,7 @@ namespace NmeaParser.Messages /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gpgst")] [NmeaMessageType("--GST")] - public class Gst : NmeaMessage + public class Gst : NmeaMessage, ITimestampedMessage { /// /// Initializes a new instance of the class. @@ -82,5 +82,7 @@ namespace NmeaParser.Messages /// Standard deviation of altitude error in meters. /// public double SigmaHeightError { get; } + + TimeSpan ITimestampedMessage.Timestamp => FixTime; } } diff --git a/src/NmeaParser/Nmea/ITimestampedMessage.cs b/src/NmeaParser/Nmea/ITimestampedMessage.cs new file mode 100644 index 0000000..1911309 --- /dev/null +++ b/src/NmeaParser/Nmea/ITimestampedMessage.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NmeaParser.Messages +{ + /// + /// Indicates this message has a time stamp + /// + public interface ITimestampedMessage + { + /// + /// Gets the time of day for the message + /// + TimeSpan Timestamp { get; } + } +} diff --git a/src/NmeaParser/Nmea/Rmc.cs b/src/NmeaParser/Nmea/Rmc.cs index df2cadf..e4c0c4a 100644 --- a/src/NmeaParser/Nmea/Rmc.cs +++ b/src/NmeaParser/Nmea/Rmc.cs @@ -28,7 +28,7 @@ namespace NmeaParser.Messages /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gprmc")] [NmeaMessageType("--RMC")] - public class Rmc : NmeaMessage + public class Rmc : NmeaMessage, ITimestampedMessage { /// /// Initializes a new instance of the class. @@ -93,5 +93,7 @@ namespace NmeaParser.Messages /// Magnetic Variation /// public double MagneticVariation { get; } + + TimeSpan ITimestampedMessage.Timestamp => FixTime.TimeOfDay; } } diff --git a/src/NmeaParser/Nmea/Zda.cs b/src/NmeaParser/Nmea/Zda.cs index 0d95eb1..91c306e 100644 --- a/src/NmeaParser/Nmea/Zda.cs +++ b/src/NmeaParser/Nmea/Zda.cs @@ -22,7 +22,7 @@ namespace NmeaParser.Messages /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Zda")] [NmeaMessageType("--ZDA")] - public class Zda : NmeaMessage + public class Zda : NmeaMessage, ITimestampedMessage { /// /// Initializes a new instance of the class. @@ -53,5 +53,7 @@ namespace NmeaParser.Messages /// Gets the time of fix /// public DateTimeOffset FixDateTime { get; } + + TimeSpan ITimestampedMessage.Timestamp => FixDateTime.TimeOfDay; } } diff --git a/src/NmeaParser/NmeaParser.csproj b/src/NmeaParser/NmeaParser.csproj index 34d1c0f..c2fabfd 100644 --- a/src/NmeaParser/NmeaParser.csproj +++ b/src/NmeaParser/NmeaParser.csproj @@ -11,7 +11,7 @@ An NMEA stream parser for serial port, bluetooth and file-based nmea simulation. nmea winrt wpf uwp xamarin gps serialport bluetooth SharpGIS.NmeaParser - 2.0 + 2.1 NMEA Parser Apache-2.0 https://dotmorten.github.io/NmeaParser/ @@ -20,7 +20,7 @@ Copyright © Morten Nielsen 2015-2020 $(MSBuildThisFileDirectory)..\..\artifacts\NmeaParser\$(Configuration) ..\..\artifacts\NuGet\$(Configuration)\ - New refined and easier to use v2 API + Added ITimestampedMessage interface to easily get time stamps from any message that supports it true true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb @@ -30,6 +30,7 @@ enable logo.png + 2.1.0.0 From d4e7b37c63e64c8afb7bc6b3308f606a4b1af843 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Wed, 22 Jul 2020 10:57:26 -0700 Subject: [PATCH 05/62] Fixed typo --- src/NmeaParser/Nmea/Rmb.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NmeaParser/Nmea/Rmb.cs b/src/NmeaParser/Nmea/Rmb.cs index f3ee0e8..0635c02 100644 --- a/src/NmeaParser/Nmea/Rmb.cs +++ b/src/NmeaParser/Nmea/Rmb.cs @@ -21,7 +21,7 @@ namespace NmeaParser.Messages /// Recommended minimum navigation information /// /// - /// Navigation data from present position to a destination waypoint provided by a Loran-C, GNSS, DECCA, navigatin computer + /// Navigation data from present position to a destination waypoint provided by a Loran-C, GNSS, DECCA, navigation computer /// or other integrated navigation system. /// /// This sentence always accompanies and sentences when a destination is active when provided by a Loran-C or GNSS receiver, From 16b3a36dcac412958c42f7fd2633084305380a56 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Sat, 25 Jul 2020 20:11:51 -0700 Subject: [PATCH 06/62] Ensure reads aren't performed after disposing --- src/NmeaParser/BufferedStreamDevice.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/NmeaParser/BufferedStreamDevice.cs b/src/NmeaParser/BufferedStreamDevice.cs index fc5c0da..3099fe5 100644 --- a/src/NmeaParser/BufferedStreamDevice.cs +++ b/src/NmeaParser/BufferedStreamDevice.cs @@ -72,6 +72,7 @@ namespace NmeaParser // in lastLineRead by lastLineRead at a steady stream private class BufferedStream : Stream { + private bool isDisposed; private readonly StreamReader m_sr; private byte[] m_buffer = new byte[0]; private readonly System.Threading.Timer m_timer; @@ -96,6 +97,9 @@ namespace NmeaParser while (groupToken == null && (lastLineRead == null || !lastLineRead.StartsWith("$", StringComparison.Ordinal))) { lastLineRead = ReadLine(); //seek forward to first nmea token + if (isDisposed) + return; + if(lastLineRead != null) AppendToBuffer(lastLineRead); } if(groupToken == null && lastLineRead != null) @@ -105,7 +109,7 @@ namespace NmeaParser groupToken = values[0]; } lastLineRead = ReadLine(); - while (!lastLineRead.StartsWith(groupToken, StringComparison.Ordinal)) //keep reading until messages start repeating again + while (!isDisposed && lastLineRead?.StartsWith(groupToken, StringComparison.Ordinal) == false) //keep reading until messages start repeating again { AppendToBuffer(lastLineRead); lastLineRead = ReadLine(); @@ -122,8 +126,10 @@ namespace NmeaParser m_buffer = newBuffer; } } - private string ReadLine() + private string? ReadLine() { + if (isDisposed) + return null; if (m_sr.EndOfStream) m_sr.BaseStream.Seek(0, SeekOrigin.Begin); //start over return m_sr.ReadLine() + '\n'; @@ -194,13 +200,14 @@ namespace NmeaParser { throw new NotSupportedException(); } - + /// protected override void Dispose(bool disposing) { - base.Dispose(disposing); - m_sr.Dispose(); + isDisposed = true; m_timer.Dispose(); + m_sr.Dispose(); + base.Dispose(disposing); } } } From 6fb5fe38fed0f638b8963ea8faadc3c05347221e Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Sun, 26 Jul 2020 09:20:06 -0700 Subject: [PATCH 07/62] Fix expired certificate --- .../NmeaParser.Tests.UWP.csproj | 7 ++++--- .../NmeaParser.Tests.UWP_TemporaryKey.pfx | Bin 2528 -> 2536 bytes 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/UnitTests/NmeaParser.Tests.UWP/NmeaParser.Tests.UWP.csproj b/src/UnitTests/NmeaParser.Tests.UWP/NmeaParser.Tests.UWP.csproj index a115610..896c65f 100644 --- a/src/UnitTests/NmeaParser.Tests.UWP/NmeaParser.Tests.UWP.csproj +++ b/src/UnitTests/NmeaParser.Tests.UWP/NmeaParser.Tests.UWP.csproj @@ -18,9 +18,10 @@ {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} NmeaParser.Tests.UWP_TemporaryKey.pfx $(VisualStudioVersion) - D07B149B4E796AB0184B2E2FC9DDC1A2F5CA5A7E - 8.0 - enable + 59902E8FFCA0C735A9FB2184201D87DF7DBF401B + 8.0 + enable + True true diff --git a/src/UnitTests/NmeaParser.Tests.UWP/NmeaParser.Tests.UWP_TemporaryKey.pfx b/src/UnitTests/NmeaParser.Tests.UWP/NmeaParser.Tests.UWP_TemporaryKey.pfx index d95221ada7d84950fa8d26c1026fe8f0012c9fcf..a0759befa9c61dcb95501a5ab7b9730a977b6e99 100644 GIT binary patch delta 2351 zcmV+~3DEZ76X+8nFoFr>0s#Xsf(f7o2`Yw2hW8Bt2LYgh36TVX35_s<35k&+Mt|?- z-XX4}DU|{O2haq91lal|#UIJaH&x95;^UsNCIsSRkH{bEcTF_*AwD44GQOA9g>6_| zJC?DcZoWyj^nPO5KhYwV)qKagG^JEeEc~$Y3gP`iF+4POCoEKjeoTv}0><-yuVt*>cki4sL z{hqbADl}n*^OBUCey9;$%lT5;*3l$B#gcubtTo?W4l)P_RT-Kj;L-yJ}!m|hP z(3y9y9u)^12-J$GlW?paYOSm?ybu}nh{ty>LJUi78Y*##vH}9taY486J_%?Nm#fwTKGp7xuKDvr9Qu`#+>uLV z0^MbkvSY_(lIp#lw!MRZrs?^aQ$R%VhpHRdR>nWP_EW2fwgeBWGJx%;q6WYNCoVJ> zld*A2n*xQ{&-OBqRMo|+{H7P$yu{ek)7{Ew{=xEEyIF5TVT$JtSr>G-cp23rh z6EBe2R}OoFHMrjMllAOclYbtpJU;%8)js`pm_*@#D7AXuLVJ!V7}ypxa|KDNTVEEZ zGb)%*+Jb$0G3wOspgB-_7Re)+Tk|4_JX^Z|th4b;5lk*Gb2}&Ve5i^t?k z^) z?P*rKd?@PELL6UD;j7bWZFwgbxTC#j>EL|RedxGm-19gUxIz41FTLv}#Q<{VK+7Q& zzfPhsiWhFx6Mw0xaamK^H*6zP)VRO$1espoAH?l|@S$5{l`|hCx}P3189cy=VsN`- z{vCs<%Dy*ho;u;9dz{0RoJ<-e49-UI@tIh=QFXtoW|YM)U)O)BtTMr)L&tx0F15vN zc$W|Z39BHTT=7A9B2}&IBc{UAF2ke#Al-}kzA1ix^nWBzrYUf#!<-E-FhK9MVQGkC1Lsn)v;)WDQ%2CVJa-KjtAW1NwM#8or2>UqXsdYJrxxUtB~`&8&Mf!5KPc8u92Fs zH?hvE`hW0>SHQ(zTw-Fo+v4QmV}Md|3sN$T1e2)IfX5A4@xoJE-i|^>IRr{mxxQ%M ztQiM2GJYV3bEAPRnyLuZTp#3L+v18xT%JG~Do(_|`4m+<;YzLeHT%o7p&Vy#sb!aJ zN9CYG5UV&d2^}9+tuF2{fz2=z1_>&LNQUu?SlurHwqRWd_jTv>4eHx>ub`1dnYJ{qzZ`(UU59cQj(!u0z%ds z%3ZfY@K?$<_#(CMD3J51M!ifOTaTE-X&}d|Mkay^ekZdp2s|PQYz`vC(vLTf5rWDxp~@C2k%y8% zvX|ImU501QclrX`#KGzn3GPqh3?nS`4hM)YbtHO>p?_#J@#gx5&|ulhl=1IJ2)-u0 zdN-&BTfYMCzXxeXtuUHl*D$wzjYv=u=-5!a^9MfQZ&vB8UiBv6f>5_b-)_- z&_t-+O2xYr(+dSroKlUvzRb`LO1gkjaDV!f+<#}P@H?OQ(TAMDcNlA#CbO4kstHD$9EZ9to%8A-B$q0aoS@0 z_8@I$VmozK38@KfQfYExUc0SQK3w=6z<-Vq{Ie#~mn@}&x;KZa=p}6oq!tn21EgWc z{i=LO-J%hdQmRekFpUhckaPAJ#Ih{dGAe8ntpKHz8afhMAAMuQ7!IAAf+n5Z7GpRk z0;~!UAFkQJFgq|GFb4(&D-Ht!8Uz%s3w;wt_yAV?4kp%ia~ V5()lP`ef!PVF`UX;pGAX2hjaXPmurs delta 2343 zcmV+?3E1}N6W|jfFoFr(0s#Xsf(e)g2`Yw2hW8Bt2LYgh35f)P3576%34xI!Mt{0x z+fE|0oTvf<2haq91lZ;i^j?$54WnTGL_PA<&K^YH>3mTlUmCUc=&VI>uOK1pug|M^ zJVVThUUAelm6btWlv&jasfQCKmqS4L2&le5ZVE+du! z$)}(fXtj$5%yAb9*xs(&DEgxGkwqT*ouu=-dy(BUp>;?P>&-yyUA|04Z_$N&@E^qL3 z7R#RUHqIc90<_Bgb0#}vKY#xAldE6N$E{oGVO#&ZPjtcYGnAHwxo@oUeGiOrvaX{}C@ z!M@=L0Pi#D6oV7MW`7qER-H|XUbiVr!H)Fr01@zu?$w6aGdoBws! zA@H&^(kjS)A`4S6DkIsASX8kd**WqkUptePzV0&!?1ROn1$+V6*>KU5NT2U+wi9k2 z^yMJb%Q8=~*=%;3T9?LD5Yn+ToZ*>C=K_dM*};B56P;m2SAPZL@Gmfel=ZQ}b#s4K zI;ONN6in!+YGEBZd(;gN2EROCxTPWv{`Nkr_NRY>GVCk6Ok~f6mr%eCrPq1?Bt-f9 zCmU;ryl2p|3B>oBS;X~4_(n#HwzCL>i)mGunS_i89yPLt+NK2OBkLp((BqaRc3B@0 zSr8qAsfCG^34d+w0B)sk9ZMLU?`NUrC4)Idt(k#FJ@`l@){%!xe}33NL*B3)mikvz zFS*BzG&=rabQBV%`WYZ5m0f_3yG`0{8}N$DL}dP^9Iz>dT#w*W0UHJ2=6Tq>Hwd`e^Ksjxu$3daNbCY$BLj}gjh^V+ z*GIJ}VwoM^>??|hZTP`$Fd8#eyk9Z>RD%{`c zUAw=7&LNQUA~yVTObi(m6l2E6n$>gHm=hx#RV0?E)=bl!kWle+8@jK0ubX+i(17Z!_$R7EOXuzIkeaUL} zvmD2&<6wzEraVInuAo-AzG2}TZ+avX`Dv|P;?38iA%A7fer2gNA%oy7P2oX+>izWf zO0f(14S2b8`wK2K%3vT|=}I(8+%A)e_wq2aRgr4BLz%I#Me$1*frpXmticP&vw0NF z@HKH-ASFlpCQlRjmcNya>2~Ja)m}rCisy7RX?Yjcc14bnC}(N(o;y4KK_EZsB+#NaRN|1d;n*&AFVEC=@ZT~v_R;BSr1~n z-~oR;bxMQwY0nnWn}_XB5$*ku4ylrAp-GQg7Jq*ubGwqGKAvwuLRHY1`8uN9Smq@BKcYfo7ZhGb!1!OwhTmi4&AXlN>nx0VTUH2nveqkjjBFQS}ruS3l;=bb-qL&GAb3!$_6lmRGn5GGWe zNq>FGjH$9VLqWH-$$_ahWj(n%5EpI=30g9FWxLlVU**&#B{155-~YnRpdR(#S{00k zWP_~%*aMihB`@*>p3T=gB?qT9? z(t|~4TRAlVN`Pe)`)K3f18-9jvVSA-gWd!i@QJR5yk7M9`Q&| zFgq|GFb4(&D-Ht!8Uz#^-D*prY}#}lUK9)cDumzU->6rCD7H!2LzN+F_uM+`9z NO4K9j%e4Xm2hgB|Mdkni From 9365d793648eef31a5fd3ade27c3001ed7ae68af Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Sun, 26 Jul 2020 09:20:19 -0700 Subject: [PATCH 08/62] Upgrade sourcelink package to 1.0.0 --- src/NmeaParser/NmeaParser.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NmeaParser/NmeaParser.csproj b/src/NmeaParser/NmeaParser.csproj index c2fabfd..43d6c91 100644 --- a/src/NmeaParser/NmeaParser.csproj +++ b/src/NmeaParser/NmeaParser.csproj @@ -67,7 +67,7 @@ - + From b52d2d2d2b1a25f9ac7974702db3083a82d5adf2 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Sun, 26 Jul 2020 09:45:22 -0700 Subject: [PATCH 09/62] Update expired certificate --- src/SampleApp.UWP/SampleApp.UWP.csproj | 3 ++- .../SampleApp.UWP_TemporaryKey.pfx | Bin 2528 -> 2536 bytes 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SampleApp.UWP/SampleApp.UWP.csproj b/src/SampleApp.UWP/SampleApp.UWP.csproj index 6182b26..1fb996d 100644 --- a/src/SampleApp.UWP/SampleApp.UWP.csproj +++ b/src/SampleApp.UWP/SampleApp.UWP.csproj @@ -18,7 +18,8 @@ 512 {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} SampleApp.UWP_TemporaryKey.pfx - D89DCA9AB58E8FD65E1F0E01AF1A7AFC277C1706 + F2A07DA2EDDDF050675AAFAB0507AA6E66A355F0 + True true diff --git a/src/SampleApp.UWP/SampleApp.UWP_TemporaryKey.pfx b/src/SampleApp.UWP/SampleApp.UWP_TemporaryKey.pfx index a2b80d563915cb0ed667e2bf7a7af4f1dbc7bc90..b8e685418e4b05cf653b5708e584c10b440c311d 100644 GIT binary patch delta 2369 zcmV-H3BLB=6X+9=C4ce;2`Yw2hW8Bt2LYgh1>*#Q1>Z1&1=}!!1=j`(Duzgg_YDCI z3IPJ3f&}(3f&}s~90m$1hDe6@4FL=R127H*2w-^VTS@!Nhynr!&;)`6(AevY1=BF| zR$v2g06$=h*OotK)Q+b8CiPAx-NH#Qi>MEfvk}-uA^_=4(|^13F+uNfj2m@*W7|}= z=cXq&-0rzeheh4&O&06TdCBC!oM`s=$R4fl{F~6nO$winx4C5rbClcm;@~)v$C&QF9WUplMD}OYViKC1zHPd79U$JY26*(J>dzxm*yKV38P1CYX^StB!c!pl2 z!oX1?>2B9Q&MFjh{e7TJvvg0dt?7F4f}49K9e+FsW$!c0gX(hQcvmb%$WOA&9o$9J z(nRjK(AX}vwRl(33%mZ@K)(NLl+R0Yv?Ag?i&Bw*RG^C|%#3cr;;w5iUv{Xe5KS2U zzLnb(-r1X;3&a*Mejf67cYmbk(L0ovIx09NuGl*EjUTlZp)&6%Vm-_FJH0 zPk)_G%tLi7RRZ_Az9b~hPIZ%Vs;W%LOW$&i)C@vE6z#fM0Y6oK9lp{bvrDb2?5P{q&8TQ1S({GBzdmaS&N6-g`9}Xv{3fDPP`Ymy7FL*ZrC4z-(0}@F zwB|6N8WJkrQoqF1$IzsE45AEi3>}#xyUs5Io+pw&;PsP0k$6)15iT{5aw-RUJ^117 ztrL*@0b69p27XK(lU`L*+&t>?y6jhv{a-Z3vGC^VAkY%a*krUU)-u#sviqwu7-t;)^%chu80e=YkrRQRv3M@(#{QhJGkPxMLhU3^>_pMMY!|Y^$ zxVI|JC6AvK>f&ZFLZNAY@ARQ<_0;_83KHp( zr6=G0%bN*SNDMK-7fgJG@IUEEJ*t8!DFfAD%9y-Za z0EQYX8Hzzq+itj~E@bp|C|Or2ETML0vZVgBG3&Yn20w-M2#_w5Pz5SSW&kz-WB@h* zFaR+CF#s_DEdV$GH2`A(VE`=vGyr7)IRG#KEdXKwF#t9IGyp9CGypaLGypjOWdLFT zG5|LKH~?kMc(7ZceM*Q}u>?+U z!vrZHdr9D8Qzny$eE1ISjnaEK?PpU=OlI2#P<^2(n&swMAigUn>M~ZClOi2tA(|kp zQKmqQ`;MQzld+61TBcs})XCs~s6b0&EsN<$K_se_n9;&;zx#%q+Nkf_gMn#f3hGlq z7w2k7@b!#xU1&Nw@f}f|1xLcXh@R16$)cA8(NwB^D*hu->!*aph5g>~=o#xhMlRvp zOLF#z4&H9Io|F}7qWSU^V=2IJ%20s$!vwzNh1q+=z#UIsW{M~>M#Fx8qG}6+2;FcKcogdGCOjGOfq6#IZt>$fuv2W z6(R=%9CZ6X4K1pqeMD+#wjXf@k`@ZGUEDBN2-gv4N_sWswmHPlUf1O~MV$3!ZVUI; zc}ku#Z+#F;{8Y+O7@>`uata)%KJAAI<5{t^9-H!e`)6& z%87zU4lY58nYW{V1}O!@6FXt?UG|w0{8ox!xA_w+wJOVOR=D;l@F+lcN`$I_scd=M z?$8GSx)e0lHU(U$Y{U)63B3{vxpc)ahA)jI1t<(=ht=^9XcZ*6E0thl5G=~1h zd$YNw(lxqR;9jzmhgm+V_D07@qg`FHqBh+0cCxqK2YO+DQH0(Q=CqVn%76DF@CGgK z$^2VxQzHW5#OI}Y1*Z8bf{2GY+m%~qql+1P;E9JD69*+&L|g-jz;{iqhTRSarCj@7 znovTot(jSrKC#hqkfU8RbI33|Fdr}n1_dh)0|FWZ6nm;)OTW#XaRKD71#SL7OtHB+ nLj)8Lg>TdxTkFSw*D<&blte?1$<`Ow0s;rn0000000000!u?A| delta 2385 zcmZWpX*|@68lIIRvP{R8%#1C|WDG)Msc4#H8_Zb3r0fYJ3dQ7ai9us6vNRNBS0kr1 zM8yavj6^zeWN(ZmsjDH^xxagV=fnN*yzldSp6BEHKJ&7F$!4k3e~RqZP9diJkN}H< zSpb|51K@L506w*Y2@&!CT6@I65EcOYg#}SV`C}kd(3Z#qTdfAP~_3 z7yuh}M<@XJVlISEWrn37n$ zb!$vZ+BIohOG~1hUCms2*@r$lW=Qn0arRkpJ-p(#w7zoxz$$&3xs6>lMX4F3pU0$`da|$X)hEsGstK6z6cY^G${+6UA3^&2$rwxa^zo`nH$f^FF=MuK^H4o z^-{wG-orPflbjXT9!BFAXQwX0n5w{J&$}CV^Mv|@Aty{hwNK7*yG6P|rBqiUM?c2O zt1Fbx=S!sr587ua?#C-RIBYbXu+E+0byt#iGd&~B6$?^zDF^pB4E9d5V)~4l{Kq^Q za&lPyjZ0c`dT-sp$s{pGsM`QfsjC1jn-kPZLHy1u=E?oMf z_3Ls3|JVt{HTjQ)G`h?f5qlo8$cLEJ<&4LdJR6Xee){S2{L?7gH#0Pp(|rBGJ7Y&} zcekD{mPB6(+Tb;>MScwfXj+53-#s#-Ta)S}YGQ5$Tm!}nzM|u!_nv(8{xjQd`*!;G zEl{JD^4M!ns_)cnC;Cq3p+CPtyt5X*8J3dp*LBOAHdx=)ms#|VpOLK#+rytEx)67U zMw9%sF1E>y{Qcg+C_fQPt00)09p@Qlux1TiFA@q>tR=p(;!{g68~AsI8PJy0sm%-3=7U zG6flB8{s(fkuzSMiS>XuL#x=RsQjU$>+N!LGgo*c#Lw_j=RK@CID8k`M>iEw6ibfp zJ>>BPwO}0hE*x&D+SO0~FW!vQBJvdA$Pr!kkw4``_IQc^>M93_8_4IGV4|bDf*5?wsxX0DkKUoN?A@N$%?B&;vN(Vd?yp;#eMaVICJuf zOxS12#2@C_zXiUMPg*ng*>>N5ZYntLWKt46ty#j>zkt{(c3EqmE0XETaXxojL; zSjUSFa-*fJ6kc6P6}gDK zE~S0;o8B;o59og1N97It;;$|^N9A9M?4`_#mIQs@*vL6M-OIiiMOum~x`Tg2-Dsco z%O%3KPa1&?K*pf+JJV-p`+zW@!yqik1Z1?s3_-pi9LRKsne2E5|6zwg=XO%q9q5Dn zc2wV;qzOnr_Zj>go&HY?i?9G_I2M40K)|4#<@b*R{{QNowrZUJb*J0o@9Mn{Kuw}r zEK)iItl(>q`gMj4@jVeKE>bvLQy8;${ONm`-C|Wc^RwxJeO}{MWmF>#wQ9r)+LFXR zLO*~mvq6g@ksoo?OhD`*cpN9LH#23%`MoTQV^VJ7{;G3aN3lO=L~CsyBIyXnJ}2`b z#Gq#)Bs3G%8`$XK^$`AjNJT%JKwkjooii(s<4qnG$Ok9;$i%SxR7hQm3c7Dgd;T)a zJore-JQ}|}?|R>BNn2)OeD9XdNJ;21x{iTEy#xm+>ygQ;I`+l};3RM3(h0hj&H9bX zPK(x({I4qKy8QsO=+>={Y0@o3Ex{+k(3s&=v?YX_+z+FeA}TGU$HB3P;xj>|w`WL7 zC|K>&(}~BeP2Wl$Ydd^-_5#zD9=OmisU3jv>y;Q&wVn&m6xT_ryyStS)eGomP$7=O zl?@_ullyF+C!5rA;NC0B%B%EQ2GMdfg&06fGt7dFW%yzj;pqMkS<6L<>x!H`DHjS4 zv4)Ge{@o>ybbtrS-^l`hboRjXUa!rD`$j*8B$qOM_)tz8*&;bMjDB`Clj<_GTeiyo z+f{dM(u;QteJb1e2=SFA-7QCazGa@BW$o?5nmA_6Dpj>RZrD%OzFP84m|@tFi{Z8X z5?PwoG8{8w(>PmdetIQo8P0bWp8s%1LZfm%L3{3J9DQKbd@1R6x0;Kkp!LShr7Qza zVUR`{%els^GPCgIz20c(X3t1A(Y}CUzYecFEu|-KapDO=cuCBRz9!>wFv;TKyel>p z@5V{By{jwId*=Q7=z=pSw;0OiA?c6dI_jmPpE%Ep!+MEF3YVp6>VHOF^fV1c-V8hC zPYQ~R2T89?E_)8Gu5TBt<_8lnZBm|dO%}NwWX+f1Ra0Y9n?ZcGq_FF=rabII$VLay zJ63;dO0|D3U_Hl&u8@+$&4cb=*8W-0nHIX4w>r!3VrS7>0QG#ua(SIn z3=CR7M&5!LzIaDX)|&{qQ1Uh&-3~wScwAZI0-ab~U*(KLR&V`wZb`fy-}eA%juMiA z(Jq*^+bThV^y%CB5)l^ zC`1v4;1c4dqtrB*1oa6KeqHb0h~b0;7=jS&oJYYOtgsCX*re2(QRO{yOaA`9e*-a~ BNk0Gp From abd5fe56c75918fb9a5e14ad9a6ed559d7fae969 Mon Sep 17 00:00:00 2001 From: Morten Nielsen <1378165+dotMorten@users.noreply.github.com> Date: Mon, 27 Jul 2020 22:50:26 -0700 Subject: [PATCH 10/62] Fix typo --- docs/concepts/ArcGISRuntime.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/ArcGISRuntime.md b/docs/concepts/ArcGISRuntime.md index 74d6ef6..c8e8b76 100644 --- a/docs/concepts/ArcGISRuntime.md +++ b/docs/concepts/ArcGISRuntime.md @@ -150,7 +150,7 @@ namespace NmeaParser.ArcGIS { // Vertical id 115700 == ellipsoid reference system. Gga is geoid, but we subtract GeoidalSeparation to simplify // vertical transformations from the simpler/better known ellipsoidal model - position = new MapPoint(gga.Longitude, gga.Latitude, gga.Altitude - gga.GeoidalSeparation, SpatialReference.Create(4326, 115700)); + position = new MapPoint(gga.Longitude, gga.Latitude, gga.Altitude + gga.GeoidalSeparation, SpatialReference.Create(4326, 115700)); } } if (rmc != null && rmc.FixTime.TimeOfDay == timeOfFix) From fe75ff21f1780c3955551141f7998dc8bf70177d Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 19:46:06 -0700 Subject: [PATCH 11/62] TimeSinceLastDgpsUpdate should be nullable --- src/NmeaParser/Nmea/Gga.cs | 4 ++-- src/NmeaParser/Nmea/Gns.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NmeaParser/Nmea/Gga.cs b/src/NmeaParser/Nmea/Gga.cs index 831737e..c98e456 100644 --- a/src/NmeaParser/Nmea/Gga.cs +++ b/src/NmeaParser/Nmea/Gga.cs @@ -51,7 +51,7 @@ namespace NmeaParser.Messages if (!double.IsNaN(timeInSeconds)) TimeSinceLastDgpsUpdate = TimeSpan.FromSeconds(timeInSeconds); else - TimeSinceLastDgpsUpdate = TimeSpan.MaxValue; + TimeSinceLastDgpsUpdate = null; if (message[13].Length > 0) DgpsStationId = int.Parse(message[13], CultureInfo.InvariantCulture); else @@ -116,7 +116,7 @@ namespace NmeaParser.Messages /// /// Time since last DGPS update (ie age of the differential GPS data) /// - public TimeSpan TimeSinceLastDgpsUpdate { get; } + public TimeSpan? TimeSinceLastDgpsUpdate { get; } /// /// Differential Reference Station ID diff --git a/src/NmeaParser/Nmea/Gns.cs b/src/NmeaParser/Nmea/Gns.cs index a2183eb..ed4c1d6 100644 --- a/src/NmeaParser/Nmea/Gns.cs +++ b/src/NmeaParser/Nmea/Gns.cs @@ -155,7 +155,7 @@ namespace NmeaParser.Messages if (!double.IsNaN(timeInSeconds)) TimeSinceLastDgpsUpdate = TimeSpan.FromSeconds(timeInSeconds); else - TimeSinceLastDgpsUpdate = TimeSpan.MaxValue; + TimeSinceLastDgpsUpdate = null; if (message[11].Length > 0) DgpsStationId = message[11]; @@ -247,7 +247,7 @@ namespace NmeaParser.Messages /// /// Age of differential data - if talker ID is GN, additional GNS messages follow with GP and/or GL Age of differential data /// - public TimeSpan TimeSinceLastDgpsUpdate { get; } + public TimeSpan? TimeSinceLastDgpsUpdate { get; } /// /// eference station ID1, range 0000-4095 - Null if talker ID is GN, additional GNS messages follow with GP and/or GL Reference station ID From daa6cedd0520cf61f653de3a19f5a4004cc6935d Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 19:46:50 -0700 Subject: [PATCH 12/62] Better handle display multiple systems for GSV --- src/SampleApp.WinDesktop/MainWindow.xaml | 3 +- src/SampleApp.WinDesktop/MainWindow.xaml.cs | 6 ++-- src/SampleApp.WinDesktop/SatelliteSnr.xaml | 2 +- src/SampleApp.WinDesktop/SatelliteSnr.xaml.cs | 28 +++++++++---------- .../SatelliteView.xaml.cs | 27 +++++++++--------- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/SampleApp.WinDesktop/MainWindow.xaml b/src/SampleApp.WinDesktop/MainWindow.xaml index 8fdc921..d693e8f 100644 --- a/src/SampleApp.WinDesktop/MainWindow.xaml +++ b/src/SampleApp.WinDesktop/MainWindow.xaml @@ -39,8 +39,7 @@ - + diff --git a/src/SampleApp.WinDesktop/MainWindow.xaml.cs b/src/SampleApp.WinDesktop/MainWindow.xaml.cs index 3abce34..84aa06c 100644 --- a/src/SampleApp.WinDesktop/MainWindow.xaml.cs +++ b/src/SampleApp.WinDesktop/MainWindow.xaml.cs @@ -61,7 +61,8 @@ namespace SampleApp.WinDesktop gpgsaView.Message = null; gpgllView.Message = null; pgrmeView.Message = null; - satView.GsvMessage = null; + satView.ClearGsv(); + satSnr.ClearGsv(); //Start new device currentDevice = device; currentDevice.MessageReceived += device_MessageReceived; @@ -89,7 +90,8 @@ namespace SampleApp.WinDesktop if (args.Message is NmeaParser.Messages.Gsv gpgsv) { - satView.GsvMessage = gpgsv; + satView.SetGsv(gpgsv); + satSnr.SetGsv(gpgsv); } else if (args.Message is NmeaParser.Messages.Rmc) gprmcView.Message = args.Message as NmeaParser.Messages.Rmc; diff --git a/src/SampleApp.WinDesktop/SatelliteSnr.xaml b/src/SampleApp.WinDesktop/SatelliteSnr.xaml index 7eaf2dd..410b701 100644 --- a/src/SampleApp.WinDesktop/SatelliteSnr.xaml +++ b/src/SampleApp.WinDesktop/SatelliteSnr.xaml @@ -48,7 +48,7 @@ - messages = new Dictionary(); + public void SetGsv(Gsv message) { - get { return (NmeaParser.Messages.Gsv)GetValue(GsvMessageProperty); } - set { SetValue(GsvMessageProperty, value); } + messages[message.TalkerId] = message; + UpdateSatellites(); + } + public void ClearGsv() + { + messages.Clear(); + UpdateSatellites(); } - public static readonly DependencyProperty GsvMessageProperty = - DependencyProperty.Register(nameof(GsvMessage), typeof(NmeaParser.Messages.Gsv), typeof(SatelliteSnr), new PropertyMetadata(null, OnGpgsvMessagePropertyChanged)); - - private static void OnGpgsvMessagePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private void UpdateSatellites() { - var gsv = e.NewValue as NmeaParser.Messages.Gsv; - if (gsv == null) - (d as SatelliteSnr).satellites.ItemsSource = null; - else - (d as SatelliteSnr).satellites.ItemsSource = gsv.SVs; + satellites.ItemsSource = messages.Values.SelectMany(g => g.SVs); } } } diff --git a/src/SampleApp.WinDesktop/SatelliteView.xaml.cs b/src/SampleApp.WinDesktop/SatelliteView.xaml.cs index 487d472..c4351df 100644 --- a/src/SampleApp.WinDesktop/SatelliteView.xaml.cs +++ b/src/SampleApp.WinDesktop/SatelliteView.xaml.cs @@ -27,23 +27,21 @@ namespace SampleApp.WinDesktop { InitializeComponent(); } - - public Gsv GsvMessage + Dictionary messages = new Dictionary(); + public void SetGsv(Gsv message) { - get { return (Gsv)GetValue(GsvMessageProperty); } - set { SetValue(GsvMessageProperty, value); } + messages[message.TalkerId] = message; + UpdateSatellites(); + } + public void ClearGsv() + { + messages.Clear(); + UpdateSatellites(); } - public static readonly DependencyProperty GsvMessageProperty = - DependencyProperty.Register(nameof(GsvMessage), typeof(Gsv), typeof(SatelliteView), new PropertyMetadata(null, OnGsvMessagePropertyChanged)); - - private static void OnGsvMessagePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private void UpdateSatellites() { - var gsv = e.NewValue as Gsv; - if (gsv == null) - (d as SatelliteView).satellites.ItemsSource = null; - else - (d as SatelliteView).satellites.ItemsSource = gsv.SVs; + satellites.ItemsSource = messages.Values.SelectMany(g => g.SVs); } } public class PolarPlacementItem : ContentControl @@ -107,7 +105,8 @@ namespace SampleApp.WinDesktop { case Talker.GlobalPositioningSystem: return Color.FromArgb(alpha, 255, 0, 0); case Talker.GalileoPositioningSystem: return Color.FromArgb(alpha, 0, 255, 0); - case Talker.GlonassReceiver: return Color.FromArgb(255, 0, 0, alpha); + case Talker.GlonassReceiver: return Color.FromArgb(alpha, 0, 0, 255); + case Talker.BeiDouNavigationSatelliteSystem : return Color.FromArgb(alpha, 0, 255, 255); case Talker.GlobalNavigationSatelliteSystem: return Color.FromArgb(alpha, 0, 0, 0); default: return Colors.CornflowerBlue; } From 9ee484fcc27e9657248be6c436256fd79e44733b Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 19:47:05 -0700 Subject: [PATCH 13/62] Add fallback for binding --- src/SampleApp.WinDesktop/GgaControl.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SampleApp.WinDesktop/GgaControl.xaml b/src/SampleApp.WinDesktop/GgaControl.xaml index 25e6c08..1d1d83c 100644 --- a/src/SampleApp.WinDesktop/GgaControl.xaml +++ b/src/SampleApp.WinDesktop/GgaControl.xaml @@ -32,7 +32,7 @@ - + From 325641873adb7972a69b8be5bafcb9642276471a Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 19:47:27 -0700 Subject: [PATCH 14/62] Improve responsiveness of read loop --- src/NmeaParser/NmeaDevice.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NmeaParser/NmeaDevice.cs b/src/NmeaParser/NmeaDevice.cs index c65f493..3d7acd2 100644 --- a/src/NmeaParser/NmeaDevice.cs +++ b/src/NmeaParser/NmeaDevice.cs @@ -83,9 +83,9 @@ namespace NmeaParser break; if (readCount > 0) { - OnData(buffer.Take(readCount).ToArray()); + OnData(buffer, readCount); } - await Task.Delay(50); + await Task.Yield(); } }); } @@ -150,9 +150,9 @@ namespace NmeaParser /// protected abstract Task CloseStreamAsync(Stream stream); - private void OnData(byte[] data) + private void OnData(byte[] data, int count) { - var nmea = System.Text.Encoding.UTF8.GetString(data, 0, data.Length); + var nmea = System.Text.Encoding.UTF8.GetString(data, 0, count); List lines = new List(); lock (m_lockObject) { From 3c2b9f285100027dbe92b86131878e33dab80ffd Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 19:47:35 -0700 Subject: [PATCH 15/62] Fix casing --- src/NmeaParser/Nmea/NmeaMessage.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NmeaParser/Nmea/NmeaMessage.cs b/src/NmeaParser/Nmea/NmeaMessage.cs index 2df5b5b..aafd3cd 100644 --- a/src/NmeaParser/Nmea/NmeaMessage.cs +++ b/src/NmeaParser/Nmea/NmeaMessage.cs @@ -149,7 +149,7 @@ namespace NmeaParser.Messages int checksum = -1; if (message[0] != '$') - throw new ArgumentException("Invalid nmea message: Missing starting character '$'"); + throw new ArgumentException("Invalid NMEA message: Missing starting character '$'"); var idx = message.IndexOf('*'); if (idx >= 0) { @@ -164,7 +164,7 @@ namespace NmeaParser.Messages checksumTest ^= Convert.ToByte(message[i]); } if (checksum != checksumTest) - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid nmea message: Checksum failure. Got {0:X2}, Expected {1:X2}", checksum, checksumTest)); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid NMEA message: Checksum failure. Got {0:X2}, Expected {1:X2}", checksum, checksumTest)); } string[] parts = message.Split(new char[] { ',' }); From 9a71ca4db6cfd7d1e8f79e25ca66ad87b2173fec Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 20:14:40 -0700 Subject: [PATCH 16/62] Improved location datasource to better handle multiple gps systems --- src/NmeaParser.sln | 1 + .../NmeaLocationDataSourcer.cs | 114 ++++++++++++++++++ src/SampleApp.WinDesktop/NmeaProvider.cs | 100 --------------- src/SampleApp.WinDesktop/View2D.xaml.cs | 4 +- 4 files changed, 117 insertions(+), 102 deletions(-) create mode 100644 src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs delete mode 100644 src/SampleApp.WinDesktop/NmeaProvider.cs diff --git a/src/NmeaParser.sln b/src/NmeaParser.sln index deb258b..d5e43f6 100644 --- a/src/NmeaParser.sln +++ b/src/NmeaParser.sln @@ -39,6 +39,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp.NetCore", "Sample EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution + UnitTests\NmeaParser.Tests\NmeaParser.Tests.projitems*{73efb2ef-de40-46c4-9685-745a9815c0d2}*SharedItemsImports = 5 UnitTests\NmeaParser.Tests\NmeaParser.Tests.projitems*{92cad93b-6c3b-45a0-a723-be046de50fec}*SharedItemsImports = 4 UnitTests\NmeaParser.Tests\NmeaParser.Tests.projitems*{979ae182-eb59-4181-9d45-3fd6e4817f11}*SharedItemsImports = 13 EndGlobalSection diff --git a/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs b/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs new file mode 100644 index 0000000..3be9a21 --- /dev/null +++ b/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs @@ -0,0 +1,114 @@ +using Esri.ArcGISRuntime.Geometry; +using System; +using System.Threading.Tasks; + +namespace SampleApp.WinDesktop +{ + public class NmeaLocationDataSource : Esri.ArcGISRuntime.Location.LocationDataSource + { + private NmeaParser.NmeaDevice device; + private double m_Accuracy = 0; + private double m_altitude = double.NaN; + private double m_speed = 0; + private double m_course = 0; + private bool supportGNMessages; // If device detect GN* messages, ignore all other Talker ID + private bool supportGGaMessages; //If device support GGA, ignore RMC for location + + public NmeaLocationDataSource(NmeaParser.NmeaDevice device) + { + this.device = device; + if(device != null) + device.MessageReceived += device_MessageReceived; + } + void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs e) + { + var message = e.Message; + ParseMessage(message); + } + public void ParseMessage(NmeaParser.Messages.NmeaMessage message) + { + bool isNewFix = false; + bool lostFix = false; + double lat = 0; + double lon = 0; + if (message.TalkerId == NmeaParser.Talker.GlobalNavigationSatelliteSystem) + supportGNMessages = true; + else if(supportGNMessages && message.TalkerId != NmeaParser.Talker.GlobalNavigationSatelliteSystem) + return; // If device supports combined GN* messages, ignore non-GN messages + + if (message is NmeaParser.Messages.Garmin.Pgrme rme) + { + m_Accuracy = rme.HorizontalError; + } + else if(message is NmeaParser.Messages.Gst gst) + { + Gst = gst; + m_Accuracy = Math.Sqrt(Gst.SigmaLatitudeError * Gst.SigmaLatitudeError + Gst.SigmaLongitudeError * Gst.SigmaLongitudeError); + } + else if (message is NmeaParser.Messages.Rmc rmc) + { + Rmc = rmc; + if (Rmc.Active) + { + m_speed = double.IsNaN(Rmc.Speed) ? Rmc.Speed : 0; + if (!double.IsNaN(Rmc.Course)) + m_course = Rmc.Course; + lat = Rmc.Latitude; + lon = Rmc.Longitude; + } + else + { + lostFix = true; + } + isNewFix = !supportGGaMessages; + } + else if (message is NmeaParser.Messages.Gga gga) + { + supportGGaMessages = true; + if (gga.Quality != NmeaParser.Messages.Gga.FixQuality.Invalid) + { + lat = Rmc.Latitude; + lon = Rmc.Longitude; + m_altitude = gga.Altitude + gga.GeoidalSeparation; //Convert to ellipsoidal height + } + if (gga.Quality != NmeaParser.Messages.Gga.FixQuality.Invalid || gga.Quality == NmeaParser.Messages.Gga.FixQuality.Estimated) + { + lostFix = true; + } + isNewFix = true; + } + else if (message is NmeaParser.Messages.Gsa gsa) + { + Gsa = gsa; + } + if (isNewFix) + { + base.UpdateLocation(new Esri.ArcGISRuntime.Location.Location( + !double.IsNaN(m_altitude) ? new MapPoint(lon, lat, m_altitude, wgs84_ellipsoidHeight) : new MapPoint(lon, lat, SpatialReferences.Wgs84), + m_Accuracy, m_speed, m_course, lostFix)); + } + } + private static SpatialReference wgs84_ellipsoidHeight = SpatialReference.Create(4326, 115700); + protected override Task OnStartAsync() + { + if (device != null) + return this.device.OpenAsync(); + else + return System.Threading.Tasks.Task.FromResult(true); + } + + protected override Task OnStopAsync() + { + m_Accuracy = double.NaN; + if(this.device != null) + return this.device.CloseAsync(); + else + return System.Threading.Tasks.Task.FromResult(true); + } + + public NmeaParser.Messages.Gsa Gsa { get; private set; } + public NmeaParser.Messages.Gga Gga { get; private set; } + public NmeaParser.Messages.Rmc Rmc { get; private set; } + public NmeaParser.Messages.Gst Gst { get; private set; } + } +} diff --git a/src/SampleApp.WinDesktop/NmeaProvider.cs b/src/SampleApp.WinDesktop/NmeaProvider.cs deleted file mode 100644 index d26024c..0000000 --- a/src/SampleApp.WinDesktop/NmeaProvider.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Esri.ArcGISRuntime.Geometry; -using System; -using System.Threading.Tasks; - -namespace SampleApp.WinDesktop -{ - public class NmeaLocationProvider : Esri.ArcGISRuntime.Location.LocationDataSource - { - private NmeaParser.NmeaDevice device; - double m_Accuracy = 0; - double m_altitude = double.NaN; - double m_speed = 0; - double m_course = 0; - - public NmeaLocationProvider(NmeaParser.NmeaDevice device) - { - this.device = device; - if(device != null) - device.MessageReceived += device_MessageReceived; - } - void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs e) - { - var message = e.Message; - ParseMessage(message); - } - - public void ParseMessage(NmeaParser.Messages.NmeaMessage message) - { - bool isNewFix = false; - bool lostFix = false; - double lat = 0; - double lon = 0; - if (message is NmeaParser.Messages.Garmin.Pgrme) - { - m_Accuracy = ((NmeaParser.Messages.Garmin.Pgrme)message).HorizontalError; - } - else if(message is NmeaParser.Messages.Gst) - { - Gst = ((NmeaParser.Messages.Gst)message); - m_Accuracy = Math.Sqrt(Gst.SigmaLatitudeError * Gst.SigmaLatitudeError + Gst.SigmaLongitudeError * Gst.SigmaLongitudeError); - } - else if(message is NmeaParser.Messages.Gga) - { - Gga = ((NmeaParser.Messages.Gga)message); - isNewFix = Gga.Quality != NmeaParser.Messages.Gga.FixQuality.Invalid; - lostFix = !isNewFix; - m_altitude = Gga.Altitude; - lat = Gga.Latitude; - lon = Gga.Longitude; - } - else if (message is NmeaParser.Messages.Rmc) - { - Rmc = (NmeaParser.Messages.Rmc)message; - if (Rmc.Active) - { - isNewFix = true; - m_speed = Rmc.Speed; - m_course = Rmc.Course; - lat = Rmc.Latitude; - lon = Rmc.Longitude; - } - else lostFix = true; - } - else if (message is NmeaParser.Messages.Gsa) - { - Gsa = (NmeaParser.Messages.Gsa)message; - } - if (isNewFix) - { - base.UpdateLocation(new Esri.ArcGISRuntime.Location.Location(new MapPoint(lon, lat, m_altitude, SpatialReferences.Wgs84), m_Accuracy, m_speed, m_course, false)); - } - else if (lostFix) - { - - } - } - - protected override Task OnStartAsync() - { - if (device != null) - return this.device.OpenAsync(); - else - return System.Threading.Tasks.Task.FromResult(true); - } - - protected override Task OnStopAsync() - { - m_Accuracy = double.NaN; - if(this.device != null) - return this.device.CloseAsync(); - else - return System.Threading.Tasks.Task.FromResult(true); - } - - public NmeaParser.Messages.Gsa Gsa { get; private set; } - public NmeaParser.Messages.Gga Gga { get; private set; } - public NmeaParser.Messages.Rmc Rmc { get; private set; } - public NmeaParser.Messages.Gst Gst { get; private set; } - } -} diff --git a/src/SampleApp.WinDesktop/View2D.xaml.cs b/src/SampleApp.WinDesktop/View2D.xaml.cs index bf9408a..05a05da 100644 --- a/src/SampleApp.WinDesktop/View2D.xaml.cs +++ b/src/SampleApp.WinDesktop/View2D.xaml.cs @@ -28,7 +28,7 @@ namespace SampleApp.WinDesktop } mapView.LocationDisplay.InitialZoomScale = 5000; - mapView.LocationDisplay.AutoPanMode = Esri.ArcGISRuntime.UI.LocationDisplayAutoPanMode.Navigation; + mapView.LocationDisplay.AutoPanMode = Esri.ArcGISRuntime.UI.LocationDisplayAutoPanMode.Recenter; } public NmeaDevice NmeaDevice @@ -51,7 +51,7 @@ namespace SampleApp.WinDesktop mapView.LocationDisplay.IsEnabled = false; if (newDevice != null) { - mapView.LocationDisplay.DataSource = new NmeaLocationProvider(newDevice); + mapView.LocationDisplay.DataSource = new NmeaLocationDataSource(newDevice); mapView.LocationDisplay.IsEnabled = true; } } From 6a19aaf44a6a7cfa2add30c01eff8ef22785588e Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 20:34:03 -0700 Subject: [PATCH 17/62] Improve buffered stream to more accurately emulate slow baud rates --- src/NmeaParser/BufferedStreamDevice.cs | 211 ++++++++++++++------ src/SampleApp.WinDesktop/MainWindow.xaml.cs | 2 +- 2 files changed, 150 insertions(+), 63 deletions(-) diff --git a/src/NmeaParser/BufferedStreamDevice.cs b/src/NmeaParser/BufferedStreamDevice.cs index 3099fe5..e51b9e1 100644 --- a/src/NmeaParser/BufferedStreamDevice.cs +++ b/src/NmeaParser/BufferedStreamDevice.cs @@ -14,9 +14,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace NmeaParser @@ -28,7 +31,7 @@ namespace NmeaParser public abstract class BufferedStreamDevice : NmeaDevice { private BufferedStream? m_stream; - private readonly int m_readSpeed; + private readonly BurstEmulationSettings emulationSettings = new BurstEmulationSettings(); /// /// Initializes a new instance of the class. @@ -39,10 +42,10 @@ namespace NmeaParser /// /// Initializes a new instance of the class. /// - /// The time to wait between each group of lines being read in milliseconds - protected BufferedStreamDevice(int readSpeed) + /// The time to wait between each group of lines being read in milliseconds + protected BufferedStreamDevice(int burstRate) { - m_readSpeed = readSpeed; + BurstRate = TimeSpan.FromMilliseconds(burstRate); } /// @@ -55,12 +58,53 @@ namespace NmeaParser /// protected sealed async override Task OpenStreamAsync() { - var stream = await GetStreamAsync(); + var stream = await GetStreamAsync().ConfigureAwait(false); StreamReader sr = new StreamReader(stream); - m_stream = new BufferedStream(sr, m_readSpeed); + m_stream = new BufferedStream(sr, emulationSettings); return m_stream; } + /// + /// Gets or sets the emulated baud rate. Defaults to 115200 + /// + /// + /// Note that if the baud rate gets very low, while keeping a high , the stream will not be able to keep + /// up the burstrate. For high-frequency bursts, make sure you have a corresponding high emualated baud rate. + /// + public uint EmulatedBaudRate + { + get => emulationSettings.EmulatedBaudRate; + set => emulationSettings.EmulatedBaudRate = value; + } + + /// + /// Gets or sets the emulated burst rate - that is the frequency of each burst of messages. Defaults to 1 second (1hz). + /// + /// + /// Note that if the burst rate gets very high, while keeping a low , the stream will not be able to keep + /// up the burstrate. For high-frequency bursts, make sure you have a corresponding high emualated baud rate. + /// + public TimeSpan BurstRate + { + get => emulationSettings.BurstRate; + set + { + if (value.TotalMilliseconds < 1) + throw new ArgumentOutOfRangeException(nameof(BurstRate), "Burst rate must be at least 1 ms"); + emulationSettings.BurstRate = value; + } + } + + /// + /// Gets or sets the separator between each burst of data. Defaults to . + /// + /// + public BurstEmulationSeparator BurstSeparator + { + get => emulationSettings.Separator; + set => emulationSettings.Separator = value; + } + /// protected override Task CloseStreamAsync(System.IO.Stream stream) { @@ -68,54 +112,97 @@ namespace NmeaParser return Task.FromResult(true); } + private class BurstEmulationSettings + { + public uint EmulatedBaudRate { get; set; } = 115200; + public TimeSpan BurstRate { get; set; } = TimeSpan.FromSeconds(1); + public BurstEmulationSeparator Separator { get; set; } + } + + /// + /// Defined how a burst of data is separated + /// + /// + public enum BurstEmulationSeparator + { + /// + /// The first NMEA token encountered will be used as an indicator for pauses between bursts + /// + FirstToken, + /// + /// An empty line in the NMEA stream should indicate a pause in the burst of messages + /// + EmptyLine + } + // stream that slowly populates a buffer from a StreamReader to simulate nmea messages coming // in lastLineRead by lastLineRead at a steady stream private class BufferedStream : Stream { - private bool isDisposed; private readonly StreamReader m_sr; private byte[] m_buffer = new byte[0]; - private readonly System.Threading.Timer m_timer; private readonly object lockObj = new object(); private string? groupToken = null; - private string? lastLineRead = null; + private BurstEmulationSettings m_settings; + private CancellationTokenSource m_tcs; + private Task m_readTask; + /// /// Initializes a new instance of the class. /// /// The stream. - /// The read speed in milliseconds. - public BufferedStream(StreamReader stream, int readSpeed) + /// Emulation settings. + public BufferedStream(StreamReader stream, BurstEmulationSettings settings) { + m_settings = settings; m_sr = stream; - m_timer = new System.Threading.Timer(OnRead, null, 0, readSpeed); //read a group of lines every 'readSpeed' milliseconds + m_tcs = new CancellationTokenSource(); + m_readTask = StartReadLoop(m_tcs.Token); } - private void OnRead(object state) + + private async Task StartReadLoop(CancellationToken cancellationToken) { - if (lastLineRead != null) - AppendToBuffer(lastLineRead); - //Get the group token if we don't have one - while (groupToken == null && (lastLineRead == null || !lastLineRead.StartsWith("$", StringComparison.Ordinal))) + await Task.Yield(); + var start = Stopwatch.GetTimestamp(); + while (!cancellationToken.IsCancellationRequested) { - lastLineRead = ReadLine(); //seek forward to first nmea token - if (isDisposed) - return; - if(lastLineRead != null) - AppendToBuffer(lastLineRead); + var line = ReadLine(); + if (line != null) + { + // Group token is the first message type received - every time we see it, we'll take a short burst break + if (groupToken == null && line.StartsWith("$", StringComparison.Ordinal)) + { + var values = line.Trim().Split(new char[] { ',' }); + if (values.Length > 0) + groupToken = values[0]; + } + if (m_settings.Separator == BurstEmulationSeparator.EmptyLine && string.IsNullOrWhiteSpace(line) || + m_settings.Separator == BurstEmulationSeparator.FirstToken && groupToken != null && line.StartsWith(groupToken, StringComparison.Ordinal)) + { + // Emulate the burst pause + var now = Stopwatch.GetTimestamp(); + var delay = (now - start) / (double)Stopwatch.Frequency; + if (delay < m_settings.BurstRate.TotalSeconds) + await Task.Delay(TimeSpan.FromSeconds(m_settings.BurstRate.TotalSeconds - delay)).ConfigureAwait(false); + else + { + Debug.WriteLine("Warning: baud rate too slow for amount of data, or burst rate too fast"); + } + if (cancellationToken.IsCancellationRequested) + return; + start = Stopwatch.GetTimestamp(); + } + if (!string.IsNullOrWhiteSpace(line)) + { + await AppendToBuffer(line).ConfigureAwait(false); + } + } } - if(groupToken == null && lastLineRead != null) - { - var values = lastLineRead.Trim().Split(new char[] { ',' }); - if (values.Length > 0) - groupToken = values[0]; - } - lastLineRead = ReadLine(); - while (!isDisposed && lastLineRead?.StartsWith(groupToken, StringComparison.Ordinal) == false) //keep reading until messages start repeating again - { - AppendToBuffer(lastLineRead); - lastLineRead = ReadLine(); - } } - private void AppendToBuffer(string line) + + private double pendingDelay = 0; + + private async Task AppendToBuffer(string line) { var bytes = Encoding.UTF8.GetBytes(line); lock (lockObj) @@ -125,39 +212,49 @@ namespace NmeaParser bytes.CopyTo(newBuffer, m_buffer.Length); m_buffer = newBuffer; } + var delay = bytes.Length * 10d / m_settings.EmulatedBaudRate; // 8 bits + 1 parity + 1 stop bit = 10bits per byte; + + pendingDelay += delay; + if (pendingDelay < 0.016) //No reason to wait under the 16ms - Task.Delay not that accurate anyway + { + return; + } + // Task.Delay isn't very accurate so use the stopwatch to get the real delay and use difference to fix it later + var start = Stopwatch.GetTimestamp(); + await Task.Delay(TimeSpan.FromSeconds(pendingDelay)).ConfigureAwait(false); + var end = Stopwatch.GetTimestamp(); + pendingDelay -= (end - start) / (double)Stopwatch.Frequency; } + private string? ReadLine() { - if (isDisposed) + if (m_tcs.IsCancellationRequested) return null; if (m_sr.EndOfStream) m_sr.BaseStream.Seek(0, SeekOrigin.Begin); //start over return m_sr.ReadLine() + '\n'; } - - /// - public override bool CanRead { get { return true; } } /// - public override bool CanSeek { get { return false; } } + public override bool CanRead => true; /// - public override bool CanWrite { get { return false; } } + public override bool CanSeek => false; + + /// + public override bool CanWrite => false; /// public override void Flush() { } /// - public override long Length { get { return m_sr.BaseStream.Length; } } + public override long Length => m_sr.BaseStream.Length; /// public override long Position { - get { return m_sr.BaseStream.Position; } - set - { - throw new NotSupportedException(); - } + get => m_sr.BaseStream.Position; + set => throw new NotSupportedException(); } /// @@ -184,28 +281,18 @@ namespace NmeaParser } /// - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// - public override void SetLength(long value) - { - throw new NotSupportedException(); - } + public override void SetLength(long value) => throw new NotSupportedException(); /// - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + /// protected override void Dispose(bool disposing) { - isDisposed = true; - m_timer.Dispose(); + m_tcs.Cancel(); m_sr.Dispose(); base.Dispose(disposing); } diff --git a/src/SampleApp.WinDesktop/MainWindow.xaml.cs b/src/SampleApp.WinDesktop/MainWindow.xaml.cs index 84aa06c..2d93ca0 100644 --- a/src/SampleApp.WinDesktop/MainWindow.xaml.cs +++ b/src/SampleApp.WinDesktop/MainWindow.xaml.cs @@ -36,7 +36,7 @@ namespace SampleApp.WinDesktop //var device = new NmeaParser.SerialPortDevice(portName); //Use a log file for playing back logged data - var device = new NmeaParser.NmeaFileDevice("NmeaSampleData.txt"); + var device = new NmeaParser.NmeaFileDevice("NmeaSampleData.txt") { EmulatedBaudRate = 9600, BurstRate = TimeSpan.FromSeconds(1d) }; StartDevice(device); } From c9413f2a35ec33bc8fe9076a0ab973e53bab7357 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 20:34:26 -0700 Subject: [PATCH 18/62] Fixed typos in arcgis location datasource --- src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs b/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs index 3be9a21..94f1751 100644 --- a/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs +++ b/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs @@ -50,7 +50,7 @@ namespace SampleApp.WinDesktop Rmc = rmc; if (Rmc.Active) { - m_speed = double.IsNaN(Rmc.Speed) ? Rmc.Speed : 0; + m_speed = double.IsNaN(Rmc.Speed) ? 0 : Rmc.Speed; if (!double.IsNaN(Rmc.Course)) m_course = Rmc.Course; lat = Rmc.Latitude; @@ -71,7 +71,7 @@ namespace SampleApp.WinDesktop lon = Rmc.Longitude; m_altitude = gga.Altitude + gga.GeoidalSeparation; //Convert to ellipsoidal height } - if (gga.Quality != NmeaParser.Messages.Gga.FixQuality.Invalid || gga.Quality == NmeaParser.Messages.Gga.FixQuality.Estimated) + if (gga.Quality == NmeaParser.Messages.Gga.FixQuality.Invalid || gga.Quality == NmeaParser.Messages.Gga.FixQuality.Estimated) { lostFix = true; } From 63344030ee97492444f69d8231ef0187278e6332 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 20:44:48 -0700 Subject: [PATCH 19/62] Maintain precision of accuracy --- src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs b/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs index 94f1751..10624c0 100644 --- a/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs +++ b/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs @@ -43,7 +43,8 @@ namespace SampleApp.WinDesktop else if(message is NmeaParser.Messages.Gst gst) { Gst = gst; - m_Accuracy = Math.Sqrt(Gst.SigmaLatitudeError * Gst.SigmaLatitudeError + Gst.SigmaLongitudeError * Gst.SigmaLongitudeError); + int significantDigits = (int)Math.Ceiling(-Math.Log(Math.Min(Gst.SigmaLatitudeError%1, Gst.SigmaLongitudeError%1))); + m_Accuracy = Math.Round(Math.Sqrt(Gst.SigmaLatitudeError * Gst.SigmaLatitudeError + Gst.SigmaLongitudeError * Gst.SigmaLongitudeError), significantDigits); } else if (message is NmeaParser.Messages.Rmc rmc) { From 32f745587699bba2f59996a6cee1f7482c5d6b8a Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 21:06:48 -0700 Subject: [PATCH 20/62] Improve start/stop handling of location datasource --- src/SampleApp.WinDesktop/MainWindow.xaml.cs | 1 + .../NmeaLocationDataSourcer.cs | 42 +++++++++++-------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/SampleApp.WinDesktop/MainWindow.xaml.cs b/src/SampleApp.WinDesktop/MainWindow.xaml.cs index 2d93ca0..c05606c 100644 --- a/src/SampleApp.WinDesktop/MainWindow.xaml.cs +++ b/src/SampleApp.WinDesktop/MainWindow.xaml.cs @@ -77,6 +77,7 @@ namespace SampleApp.WinDesktop ((NmeaParser.SerialPortDevice)device).Port.PortName, ((NmeaParser.SerialPortDevice)device).Port.BaudRate); } + _ = device.OpenAsync(); } private void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs args) diff --git a/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs b/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs index 10624c0..3116624 100644 --- a/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs +++ b/src/SampleApp.WinDesktop/NmeaLocationDataSourcer.cs @@ -6,20 +6,24 @@ namespace SampleApp.WinDesktop { public class NmeaLocationDataSource : Esri.ArcGISRuntime.Location.LocationDataSource { - private NmeaParser.NmeaDevice device; + private static SpatialReference wgs84_ellipsoidHeight = SpatialReference.Create(4326, 115700); + private readonly NmeaParser.NmeaDevice m_device; private double m_Accuracy = 0; private double m_altitude = double.NaN; private double m_speed = 0; private double m_course = 0; - private bool supportGNMessages; // If device detect GN* messages, ignore all other Talker ID - private bool supportGGaMessages; //If device support GGA, ignore RMC for location + private bool m_startStopDevice; + private bool m_supportGNMessages; // If device detect GN* messages, ignore all other Talker ID + private bool m_supportGGaMessages; //If device support GGA, ignore RMC for location - public NmeaLocationDataSource(NmeaParser.NmeaDevice device) + public NmeaLocationDataSource(NmeaParser.NmeaDevice device, bool startStopDevice = true) { - this.device = device; - if(device != null) - device.MessageReceived += device_MessageReceived; + if (device == null) + throw new ArgumentNullException(nameof(device)); + this.m_device = device; + m_startStopDevice = startStopDevice; } + void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs e) { var message = e.Message; @@ -32,8 +36,8 @@ namespace SampleApp.WinDesktop double lat = 0; double lon = 0; if (message.TalkerId == NmeaParser.Talker.GlobalNavigationSatelliteSystem) - supportGNMessages = true; - else if(supportGNMessages && message.TalkerId != NmeaParser.Talker.GlobalNavigationSatelliteSystem) + m_supportGNMessages = true; + else if(m_supportGNMessages && message.TalkerId != NmeaParser.Talker.GlobalNavigationSatelliteSystem) return; // If device supports combined GN* messages, ignore non-GN messages if (message is NmeaParser.Messages.Garmin.Pgrme rme) @@ -61,15 +65,15 @@ namespace SampleApp.WinDesktop { lostFix = true; } - isNewFix = !supportGGaMessages; + isNewFix = !m_supportGGaMessages; } else if (message is NmeaParser.Messages.Gga gga) { - supportGGaMessages = true; + m_supportGGaMessages = true; if (gga.Quality != NmeaParser.Messages.Gga.FixQuality.Invalid) { - lat = Rmc.Latitude; - lon = Rmc.Longitude; + lat = gga.Latitude; + lon = gga.Longitude; m_altitude = gga.Altitude + gga.GeoidalSeparation; //Convert to ellipsoidal height } if (gga.Quality == NmeaParser.Messages.Gga.FixQuality.Invalid || gga.Quality == NmeaParser.Messages.Gga.FixQuality.Estimated) @@ -89,20 +93,22 @@ namespace SampleApp.WinDesktop m_Accuracy, m_speed, m_course, lostFix)); } } - private static SpatialReference wgs84_ellipsoidHeight = SpatialReference.Create(4326, 115700); + protected override Task OnStartAsync() { - if (device != null) - return this.device.OpenAsync(); + m_device.MessageReceived += device_MessageReceived; + if (m_startStopDevice) + return this.m_device.OpenAsync(); else return System.Threading.Tasks.Task.FromResult(true); } protected override Task OnStopAsync() { + m_device.MessageReceived -= device_MessageReceived; m_Accuracy = double.NaN; - if(this.device != null) - return this.device.CloseAsync(); + if(m_startStopDevice) + return this.m_device.CloseAsync(); else return System.Threading.Tasks.Task.FromResult(true); } From 83ed2fbeb462acb1639f686efc1d2824b407aeef Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 21:07:09 -0700 Subject: [PATCH 21/62] Try/catch start/stop of device --- src/SampleApp.WinDesktop/MainWindow.xaml.cs | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/SampleApp.WinDesktop/MainWindow.xaml.cs b/src/SampleApp.WinDesktop/MainWindow.xaml.cs index c05606c..fa5c453 100644 --- a/src/SampleApp.WinDesktop/MainWindow.xaml.cs +++ b/src/SampleApp.WinDesktop/MainWindow.xaml.cs @@ -37,14 +37,14 @@ namespace SampleApp.WinDesktop //Use a log file for playing back logged data var device = new NmeaParser.NmeaFileDevice("NmeaSampleData.txt") { EmulatedBaudRate = 9600, BurstRate = TimeSpan.FromSeconds(1d) }; - StartDevice(device); + _ = StartDevice(device); } /// /// Unloads the current device, and opens the next device /// /// - private async void StartDevice(NmeaParser.NmeaDevice device) + private async Task StartDevice(NmeaParser.NmeaDevice device) { //Clean up old device if (currentDevice != null) @@ -77,7 +77,7 @@ namespace SampleApp.WinDesktop ((NmeaParser.SerialPortDevice)device).Port.PortName, ((NmeaParser.SerialPortDevice)device).Port.BaudRate); } - _ = device.OpenAsync(); + await device.OpenAsync(); } private void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs args) @@ -121,26 +121,40 @@ namespace SampleApp.WinDesktop } //Browse to nmea file and create device from selected file - private void OpenNmeaLogButton_Click(object sender, RoutedEventArgs e) + private async void OpenNmeaLogButton_Click(object sender, RoutedEventArgs e) { var result = nmeaOpenFileDialog.ShowDialog(); if (result.HasValue && result.Value) { var file = nmeaOpenFileDialog.FileName; var device = new NmeaParser.NmeaFileDevice(file); - StartDevice(device); + try + { + await StartDevice(device); + } + catch(System.Exception ex) + { + MessageBox.Show("Failed to start device: " + ex.Message); + } } } //Creates a serial port device from the selected settings - private void ConnectToSerialButton_Click(object sender, RoutedEventArgs e) + private async void ConnectToSerialButton_Click(object sender, RoutedEventArgs e) { try { var portName = serialPorts.Text as string; var baudRate = int.Parse(baudRates.Text); var device = new NmeaParser.SerialPortDevice(new System.IO.Ports.SerialPort(portName, baudRate)); - StartDevice(device); + try + { + await StartDevice(device); + } + catch (System.Exception ex) + { + MessageBox.Show("Failed to start device: " + ex.Message); + } } catch(System.Exception ex) { From 9db4db4212b59abf7cc0be8a74576ad1014842fc Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 21:07:32 -0700 Subject: [PATCH 22/62] FIx 2D/3D view issues --- src/SampleApp.WinDesktop/View2D.xaml | 2 +- src/SampleApp.WinDesktop/View2D.xaml.cs | 10 ++++++++-- src/SampleApp.WinDesktop/View3D.xaml.cs | 5 +++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/SampleApp.WinDesktop/View2D.xaml b/src/SampleApp.WinDesktop/View2D.xaml index 20ecbd3..f212fe4 100644 --- a/src/SampleApp.WinDesktop/View2D.xaml +++ b/src/SampleApp.WinDesktop/View2D.xaml @@ -10,7 +10,7 @@ - + diff --git a/src/SampleApp.WinDesktop/View2D.xaml.cs b/src/SampleApp.WinDesktop/View2D.xaml.cs index 05a05da..ab4a152 100644 --- a/src/SampleApp.WinDesktop/View2D.xaml.cs +++ b/src/SampleApp.WinDesktop/View2D.xaml.cs @@ -29,6 +29,12 @@ namespace SampleApp.WinDesktop mapView.LocationDisplay.InitialZoomScale = 5000; mapView.LocationDisplay.AutoPanMode = Esri.ArcGISRuntime.UI.LocationDisplayAutoPanMode.Recenter; + this.IsVisibleChanged += View2D_IsVisibleChanged; + } + + private void View2D_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) + { + mapView.LocationDisplay.IsEnabled = IsVisible; } public NmeaDevice NmeaDevice @@ -51,8 +57,8 @@ namespace SampleApp.WinDesktop mapView.LocationDisplay.IsEnabled = false; if (newDevice != null) { - mapView.LocationDisplay.DataSource = new NmeaLocationDataSource(newDevice); - mapView.LocationDisplay.IsEnabled = true; + mapView.LocationDisplay.DataSource = new NmeaLocationDataSource(newDevice, false); + mapView.LocationDisplay.IsEnabled = IsLoaded; } } } diff --git a/src/SampleApp.WinDesktop/View3D.xaml.cs b/src/SampleApp.WinDesktop/View3D.xaml.cs index 42b91f3..2e21fec 100644 --- a/src/SampleApp.WinDesktop/View3D.xaml.cs +++ b/src/SampleApp.WinDesktop/View3D.xaml.cs @@ -67,15 +67,16 @@ namespace SampleApp.WinDesktop symb.Width = 3; symb.Depth = 5; symb.Height = 2; symb.AnchorPosition = SceneSymbolAnchorPosition.Bottom; graphic3D.Symbol = symb; sceneView.GraphicsOverlays["Position"].Graphics.Add(graphic3D); - sceneView.CameraController = new OrbitGeoElementCameraController(sceneView.GraphicsOverlays["Position"].Graphics[0], 200); } - + private int updateId = 0; private async void UpdateLocation(double latitude, double longitude, double newHeading) { // Handle 3D updates on 3D Scene var cid = ++updateId; + if (double.IsNaN(newHeading)) + newHeading = 0; var start = graphic3D.Geometry as MapPoint; if (start == null) { From b035adda26c7e5ec6b1df34ef38beb6573b2d1e7 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 28 Jul 2020 21:07:53 -0700 Subject: [PATCH 23/62] Add early beginnings of NTRIP tab --- src/SampleApp.WinDesktop/MainWindow.xaml | 5 +- src/SampleApp.WinDesktop/MainWindow.xaml.cs | 2 +- src/SampleApp.WinDesktop/Ntrip/Carrier.cs | 23 +++ src/SampleApp.WinDesktop/Ntrip/Caster.cs | 59 ++++++++ src/SampleApp.WinDesktop/Ntrip/Client.cs | 140 ++++++++++++++++++ src/SampleApp.WinDesktop/Ntrip/NtripSource.cs | 23 +++ src/SampleApp.WinDesktop/Ntrip/NtripStream.cs | 52 +++++++ src/SampleApp.WinDesktop/NtripView.xaml | 74 +++++++++ src/SampleApp.WinDesktop/NtripView.xaml.cs | 102 +++++++++++++ 9 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 src/SampleApp.WinDesktop/Ntrip/Carrier.cs create mode 100644 src/SampleApp.WinDesktop/Ntrip/Caster.cs create mode 100644 src/SampleApp.WinDesktop/Ntrip/Client.cs create mode 100644 src/SampleApp.WinDesktop/Ntrip/NtripSource.cs create mode 100644 src/SampleApp.WinDesktop/Ntrip/NtripStream.cs create mode 100644 src/SampleApp.WinDesktop/NtripView.xaml create mode 100644 src/SampleApp.WinDesktop/NtripView.xaml.cs diff --git a/src/SampleApp.WinDesktop/MainWindow.xaml b/src/SampleApp.WinDesktop/MainWindow.xaml index d693e8f..ef5541f 100644 --- a/src/SampleApp.WinDesktop/MainWindow.xaml +++ b/src/SampleApp.WinDesktop/MainWindow.xaml @@ -75,7 +75,7 @@ - + 1200 2400 4800 @@ -91,6 +91,9 @@ + + + diff --git a/src/SampleApp.WinDesktop/MainWindow.xaml.cs b/src/SampleApp.WinDesktop/MainWindow.xaml.cs index fa5c453..0e71fbd 100644 --- a/src/SampleApp.WinDesktop/MainWindow.xaml.cs +++ b/src/SampleApp.WinDesktop/MainWindow.xaml.cs @@ -14,7 +14,7 @@ namespace SampleApp.WinDesktop public partial class MainWindow : Window { private Queue messages = new Queue(101); - private NmeaParser.NmeaDevice currentDevice; + public static NmeaParser.NmeaDevice currentDevice; //Dialog for browsing to nmea log files private Microsoft.Win32.OpenFileDialog nmeaOpenFileDialog = new Microsoft.Win32.OpenFileDialog() { diff --git a/src/SampleApp.WinDesktop/Ntrip/Carrier.cs b/src/SampleApp.WinDesktop/Ntrip/Carrier.cs new file mode 100644 index 0000000..aed0979 --- /dev/null +++ b/src/SampleApp.WinDesktop/Ntrip/Carrier.cs @@ -0,0 +1,23 @@ +// ******************************************************************************* +// * 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. +// ****************************************************************************** + +namespace NmeaParser.Gnss.Ntrip +{ + public enum Carrier : int + { + No = 0, + L1 = 1, + L1L2 = 2 + } +} diff --git a/src/SampleApp.WinDesktop/Ntrip/Caster.cs b/src/SampleApp.WinDesktop/Ntrip/Caster.cs new file mode 100644 index 0000000..639f1f5 --- /dev/null +++ b/src/SampleApp.WinDesktop/Ntrip/Caster.cs @@ -0,0 +1,59 @@ +// ******************************************************************************* +// * 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.Globalization; +using System.Net; + +namespace NmeaParser.Gnss.Ntrip +{ + public class Caster : NtripSource + { + internal Caster (string[] d) + { + var a = d[1].Split(':'); + Address = IPAddress.Parse(a[0]); + Port = int.Parse(a[1]); + Identifier = d[3]; + Operator = d[4]; + SupportsNmea = d[5] == "1"; + CountryCode = d[6]; + Latitude = double.Parse(d[7], CultureInfo.InvariantCulture); + Longitude = double.Parse(d[8], CultureInfo.InvariantCulture); + FallbackAddress = IPAddress.Parse(d[9]); + } + + public Caster(IPAddress address, int port, string identifier, string _operator, bool supportsNmea, string countryCode, double latitude, double longitude, IPAddress fallbackkAddress) + { + Address = address; + Port = port; + Identifier = identifier; + Operator = _operator; + SupportsNmea = supportsNmea; + CountryCode = countryCode; + Latitude = latitude; + Longitude = longitude; + FallbackAddress = fallbackkAddress; + } + + public IPAddress Address { get; } + public int Port { get; } + public string Identifier { get; } + public string Operator { get; } + public bool SupportsNmea { get; } + public string CountryCode { get; } + public double Latitude { get; } + public double Longitude { get; } + public IPAddress FallbackAddress { get; } + } +} diff --git a/src/SampleApp.WinDesktop/Ntrip/Client.cs b/src/SampleApp.WinDesktop/Ntrip/Client.cs new file mode 100644 index 0000000..9c1b540 --- /dev/null +++ b/src/SampleApp.WinDesktop/Ntrip/Client.cs @@ -0,0 +1,140 @@ +// ******************************************************************************* +// * 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.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace NmeaParser.Gnss.Ntrip +{ + public class Client : IDisposable + { + private readonly string _host; + private readonly int _port; + private string? _auth; + private Socket? sckt; + private bool connected; + private Task? runningTask; + + public Client(string host, int port) + { + _host = host; + _port = port; + } + + public Client(string host, int port, string username, string password) : this(host, port) + { + _auth = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(username + ":" + password)); + } + + public IEnumerable GetSourceTable() + { + string data = ""; + byte[] buffer = new byte[1024]; + using (var sck = Request("")) + { + int count; + while ((count = sck.Receive(buffer)) > 0) + { + data += System.Text.Encoding.UTF8.GetString(buffer, 0, count); + } + } + var lines = data.Split('\n'); + List sources = new List(); + foreach (var item in lines) + { + var d = item.Split(';'); + if (d.Length == 0) continue; + if (d[0] == "ENDSOURCETABLE") + break; + if (d[0] == "CAS") + { + sources.Add(new Caster(d)); + } + else if (d[0] == "STR") + { + sources.Add(new NtripStream(d)); + } + } + return sources; + } + + private Socket Request(string path) + { + var sckt = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + sckt.Blocking = true; + sckt.Connect(_host, _port); + + string msg = $"GET /{path} HTTP/1.1\r\n"; + msg += "User-Agent: NTRIP ntripclient\r\n"; + if (_auth != null) + { + msg += "Authorization: Basic " + _auth + "\r\n"; + } + msg += "Accept: */*\r\nConnection: close\r\n"; + msg += "\r\n"; + + byte[] data = System.Text.Encoding.ASCII.GetBytes(msg); + sckt.Send(data); + return sckt; + } + + public void Connect(string strName) + { + if (sckt != null) throw new Exception("Connection already open"); + sckt = Request(strName); + connected = true; + runningTask = Task.Run(ReceiveThread); + } + + private async Task ReceiveThread() + { + byte[] buffer = new byte[65536]; + + while (connected && sckt != null) + { + int count = sckt.Receive(buffer); + if (count > 0) + { + DataReceived?.Invoke(this, buffer.Take(count).ToArray()); + } + await Task.Delay(10); + } + sckt?.Shutdown(SocketShutdown.Both); + sckt?.Dispose(); + sckt = null; + } + + public Task CloseAsync() + { + if (runningTask != null) + { + connected = false; + var t = runningTask; + runningTask = null; + return t; + } + return Task.CompletedTask; + } + + public void Dispose() + { + _ = CloseAsync(); + } + + public event EventHandler? DataReceived; + } +} diff --git a/src/SampleApp.WinDesktop/Ntrip/NtripSource.cs b/src/SampleApp.WinDesktop/Ntrip/NtripSource.cs new file mode 100644 index 0000000..8cc71da --- /dev/null +++ b/src/SampleApp.WinDesktop/Ntrip/NtripSource.cs @@ -0,0 +1,23 @@ +// ******************************************************************************* +// * 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. +// ****************************************************************************** + +namespace NmeaParser.Gnss.Ntrip +{ + public class NtripSource + { + protected NtripSource() + { + } + } +} diff --git a/src/SampleApp.WinDesktop/Ntrip/NtripStream.cs b/src/SampleApp.WinDesktop/Ntrip/NtripStream.cs new file mode 100644 index 0000000..9893109 --- /dev/null +++ b/src/SampleApp.WinDesktop/Ntrip/NtripStream.cs @@ -0,0 +1,52 @@ +// ******************************************************************************* +// * 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.Gnss.Ntrip +{ + public class NtripStream : NtripSource + { + internal NtripStream(string[] d) + { + Mountpoint = d[1]; + Identifier = d[2]; + Format = d[3]; + FormatDetails = d[4]; + if (int.TryParse(d[5], out int carrier)) + Carrier = (Carrier)carrier; + else + { + + } + Network = d[7]; + CountryCode = d[8]; + Latitude = double.Parse(d[9], CultureInfo.InvariantCulture); + Longitude = double.Parse(d[10], CultureInfo.InvariantCulture); + SupportsNmea = d[11] == "1"; + } + + public string Mountpoint { get; } + public string Identifier { get; } + public string Format { get; } + public string FormatDetails { get; } + public Carrier Carrier { get; } + public string Network { get; } + public string CountryCode { get; } + public double Latitude { get; } + public double Longitude { get; } + public bool SupportsNmea { get; } + } +} diff --git a/src/SampleApp.WinDesktop/NtripView.xaml b/src/SampleApp.WinDesktop/NtripView.xaml new file mode 100644 index 0000000..97d31ff --- /dev/null +++ b/src/SampleApp.WinDesktop/NtripView.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + +