From 73dbdf508fc9929d5d1ace25d5fe46b8aaf4a6cd Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Wed, 29 Jul 2020 16:45:16 -0700 Subject: [PATCH] Moved NTRIP classes to NmeaParser lib and introduced new GnssMonitor class for simplified monitoring of GNSS messages --- src/NmeaParser/Gnss/GnssMonitor.cs | 305 ++++++++++++++++++ .../Gnss}/Ntrip/Carrier.cs | 16 +- .../Gnss}/Ntrip/Caster.cs | 50 +++ .../Gnss}/Ntrip/Client.cs | 71 +++- .../Gnss}/Ntrip/NtripSource.cs | 8 +- .../Gnss}/Ntrip/NtripStream.cs | 42 +++ src/SampleApp.WinDesktop/GnssMonitorView.xaml | 28 ++ .../GnssMonitorView.xaml.cs | 76 +++++ src/SampleApp.WinDesktop/GsaControl.xaml.cs | 5 +- src/SampleApp.WinDesktop/MainWindow.xaml | 7 +- src/SampleApp.WinDesktop/MainWindow.xaml.cs | 6 +- .../NmeaLocationDataSource.cs | 157 ++++----- 12 files changed, 662 insertions(+), 109 deletions(-) create mode 100644 src/NmeaParser/Gnss/GnssMonitor.cs rename src/{SampleApp.WinDesktop => NmeaParser/Gnss}/Ntrip/Carrier.cs (72%) rename src/{SampleApp.WinDesktop => NmeaParser/Gnss}/Ntrip/Caster.cs (61%) rename src/{SampleApp.WinDesktop => NmeaParser/Gnss}/Ntrip/Client.cs (64%) rename src/{SampleApp.WinDesktop => NmeaParser/Gnss}/Ntrip/NtripSource.cs (76%) rename src/{SampleApp.WinDesktop => NmeaParser/Gnss}/Ntrip/NtripStream.cs (62%) create mode 100644 src/SampleApp.WinDesktop/GnssMonitorView.xaml create mode 100644 src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs diff --git a/src/NmeaParser/Gnss/GnssMonitor.cs b/src/NmeaParser/Gnss/GnssMonitor.cs new file mode 100644 index 0000000..9695c1b --- /dev/null +++ b/src/NmeaParser/Gnss/GnssMonitor.cs @@ -0,0 +1,305 @@ +// ******************************************************************************* +// * 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 NmeaParser.Messages; + +namespace NmeaParser.Gnss +{ + /// + /// Helper class for monitoring GNSS messages and combine them into a single useful location info + /// + public class GnssMonitor + { + 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 + + /// + /// Initializes a new instance of the class. + /// + /// The NMEA device to monitor for GNSS messages + public GnssMonitor(NmeaDevice device) + { + if (device == null) + throw new ArgumentNullException(nameof(device)); + Device = device; + Device.MessageReceived += NmeaMessageReceived; + } + + /// + /// Gets the NMEA device that is being monitored + /// + public NmeaDevice Device { get; } + + private void NmeaMessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs e) + { + OnMessageReceived(e.Message); + } + + /// + /// Called when a message is received. + /// + /// The NMEA message that was received + protected virtual void OnMessageReceived(NmeaMessage message) + { + bool isNewFix = false; + bool lostFix = false; + double lat = 0; + double lon = 0; + AllMessages[message.MessageType] = message; + + if (message.TalkerId == NmeaParser.Talker.GlobalNavigationSatelliteSystem) + m_supportGNMessages = true; // Support for GN* messages detected + else if (m_supportGNMessages && message.TalkerId != NmeaParser.Talker.GlobalNavigationSatelliteSystem) + return; // If device supports combined GN* messages, ignore non-GN messages + if (message is ITimestampedMessage ts) + FixTime = ts.Timestamp; + + if (message is NmeaParser.Messages.Garmin.Pgrme rme) + { + HorizontalError = rme.HorizontalError; + VerticalError = rme.VerticalError; + } + else if (message is Gst gst) + { + Gst = gst; + VerticalError = gst.SigmaHeightError; + HorizontalError = Math.Round(Math.Sqrt(Gst.SigmaLatitudeError * Gst.SigmaLatitudeError + Gst.SigmaLongitudeError * Gst.SigmaLongitudeError), 3); + } + else if (message is Rmc rmc) + { + Rmc = rmc; + if (!m_supportGGaMessages) + { + if (Rmc.Active) + { + lat = Rmc.Latitude; + lon = Rmc.Longitude; + isNewFix = true; + } + else + { + lostFix = true; + } + } + } + else if (message is Dtm dtm) + { + if (Dtm?.Checksum != dtm.Checksum) + { + // Datum change + Dtm = dtm; + Latitude = double.NaN; + Longitude = double.NaN; + IsFixValid = false; + } + } + else if (message is Gga gga) + { + Gga = gga; + m_supportGGaMessages = true; + if (gga.Quality != Gga.FixQuality.Invalid) + { + lat = gga.Latitude; + lon = gga.Longitude; + GeoidHeight = gga.GeoidalSeparation; + Altitude = gga.Altitude + gga.GeoidalSeparation; //Convert to ellipsoidal height + } + if (gga.Quality == Gga.FixQuality.Invalid || gga.Quality == Gga.FixQuality.Estimated) + { + lostFix = true; + } + isNewFix = true; + } + else if (message is Gsa gsa) + { + Gsa = gsa; + } + else if (message is Vtg vtg) + { + Vtg = vtg; + } + if (lostFix) + { + if (!IsFixValid) + { + IsFixValid = false; + LocationLost?.Invoke(this, EventArgs.Empty); + } + } + if (isNewFix) + { + Latitude = lat; + Longitude = lon; + IsFixValid = true; + LocationChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Gets a value indicating whether the current fix is valid. + /// + /// + /// If false the provided values like and are no longer current and reflect the last known location. + /// + /// + public bool IsFixValid { get; private set; } + + /// + /// Gets the latitude for the current or last known location. + /// + /// + /// + public double Latitude { get; private set; } = double.NaN; + + /// + /// Gets the longitude for the current or last known location. + /// + /// + /// + public double Longitude { get; private set; } = double.NaN; + + /// + /// Gets the geight above the ellipsoid + /// + public double Altitude { get; private set; } = double.NaN; + /// + /// Gets the Geoid Height. Add this value to to get the Geoid heights which is roughly MSL heights. + /// + public double GeoidHeight { get; private set; } = double.NaN; + + /// + /// Gets the speed in knots + /// + public double Speed => Rmc?.Speed ?? Vtg?.SpeedKnots ?? double.NaN; + + /// + /// Gets the current cource + /// + public double Course => Rmc?.Course ?? double.NaN; + + /// + /// Gets an estimate of the horizontal error in meters + /// + public double HorizontalError { get; private set; } = double.NaN; + + /// + /// Gets an estimate of the vertical error in meters + /// + public double VerticalError { get; private set; } = double.NaN; + + /// + /// Gets the horizontal dilution of precision + /// + public double Hdop => Gsa?.Hdop ?? Gga?.Hdop ?? double.NaN; + + /// + /// Gets the 3D point dilution of precision + /// + public double Pdop => Gsa?.Pdop ?? double.NaN; + + /// + /// Gets the vertical dilution of precision + /// + public double Vdop => Gsa?.Vdop ?? double.NaN; + + /// + /// Gets the latest known GSA message. + /// + public Gsa? Gsa { get; private set; } + + /// + /// Gets the latest known GGA message. + /// + public Gga? Gga { get; private set; } + + /// + /// Gets the latest known RMC message. + /// + public Rmc? Rmc { get; private set; } + + /// + /// Gets the latest known GST message. + /// + public Gst? Gst { get; private set; } + + /// + /// Gets the latest known DTM message. + /// + public Dtm? Dtm { get; private set; } + + /// + /// Gets the latest known VTG message. + /// + public Vtg? Vtg { get; private set; } + + /// + /// Gets the current fix time + /// + public TimeSpan FixTime { get; private set; } + + /// + /// Gets a list of satellite vehicles in the sky + /// + public IEnumerable Satellites => AllMessages.Values.OfType().SelectMany(s => s.SVs); + + /// + /// Gets the number of satellites in the sky + /// + public int SatellitesInView => AllMessages.Values.OfType().Sum(s => s.SatellitesInView); + + /// + /// Gets the quality of the current fix + /// + public Gga.FixQuality FixQuality => !IsFixValid ? Gga.FixQuality.Invalid : (Gga?.Quality ?? Gga.FixQuality.GpsFix); + + /// + /// Gets a list of all NMEA messages currently part of this location + /// + public Dictionary AllMessages { get; } = new Dictionary(); + + /// + /// Gets a value indicating the current Datum being used. + /// + public string Datum + { + get + { + if (Dtm == null) + return "WGS84"; + switch (Dtm.ReferenceDatumCode) + { + case "W84": return "WGS84"; + case "W72": return "WGS72"; + case "S85": return "SGS85"; + case "P90": return "PE90"; + default: return Dtm.ReferenceDatumCode; + } + } + } + + /// + /// Raised when a new location has been updated + /// + public event EventHandler? LocationChanged; + + /// + /// Raised if location tracking was lost + /// + /// + public event EventHandler? LocationLost; + } +} diff --git a/src/SampleApp.WinDesktop/Ntrip/Carrier.cs b/src/NmeaParser/Gnss/Ntrip/Carrier.cs similarity index 72% rename from src/SampleApp.WinDesktop/Ntrip/Carrier.cs rename to src/NmeaParser/Gnss/Ntrip/Carrier.cs index aed0979..4e4990e 100644 --- a/src/SampleApp.WinDesktop/Ntrip/Carrier.cs +++ b/src/NmeaParser/Gnss/Ntrip/Carrier.cs @@ -14,10 +14,24 @@ namespace NmeaParser.Gnss.Ntrip { + /// + /// Enumeration for the carrier used by the + /// public enum Carrier : int { - No = 0, + /// + /// None / unknown + /// + None = 0, + + /// + /// L1 wave + /// L1 = 1, + + /// + /// L1 and L2 waves + /// L1L2 = 2 } } diff --git a/src/SampleApp.WinDesktop/Ntrip/Caster.cs b/src/NmeaParser/Gnss/Ntrip/Caster.cs similarity index 61% rename from src/SampleApp.WinDesktop/Ntrip/Caster.cs rename to src/NmeaParser/Gnss/Ntrip/Caster.cs index 639f1f5..dd213ad 100644 --- a/src/SampleApp.WinDesktop/Ntrip/Caster.cs +++ b/src/NmeaParser/Gnss/Ntrip/Caster.cs @@ -17,6 +17,9 @@ using System.Net; namespace NmeaParser.Gnss.Ntrip { + /// + /// Gets metadata about the NTRIP Caster + /// public class Caster : NtripSource { internal Caster (string[] d) @@ -33,6 +36,18 @@ namespace NmeaParser.Gnss.Ntrip FallbackAddress = IPAddress.Parse(d[9]); } + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public Caster(IPAddress address, int port, string identifier, string _operator, bool supportsNmea, string countryCode, double latitude, double longitude, IPAddress fallbackkAddress) { Address = address; @@ -46,14 +61,49 @@ namespace NmeaParser.Gnss.Ntrip FallbackAddress = fallbackkAddress; } + /// + /// Gets the caster IP Address + /// public IPAddress Address { get; } + + /// + /// Gets the caster port + /// public int Port { get; } + + /// + /// Gets the caster identifier + /// public string Identifier { get; } + + /// + /// Gets the caster operator + /// public string Operator { get; } + + /// + /// Gets a value indicating whether it supports NMEA + /// public bool SupportsNmea { get; } + + /// + /// Gets the country code for the caster origin + /// public string CountryCode { get; } + + /// + /// Gets the latitude for the caster + /// public double Latitude { get; } + + /// + /// Gets the longitude for the caster + /// public double Longitude { get; } + + /// + /// Gets the fallback address for the caster + /// public IPAddress FallbackAddress { get; } } } diff --git a/src/SampleApp.WinDesktop/Ntrip/Client.cs b/src/NmeaParser/Gnss/Ntrip/Client.cs similarity index 64% rename from src/SampleApp.WinDesktop/Ntrip/Client.cs rename to src/NmeaParser/Gnss/Ntrip/Client.cs index 830c207..a519ab5 100644 --- a/src/SampleApp.WinDesktop/Ntrip/Client.cs +++ b/src/NmeaParser/Gnss/Ntrip/Client.cs @@ -11,7 +11,6 @@ // * See the License for the specific language governing permissions and // * limitations under the License. // ****************************************************************************** -#nullable enable using System; using System.Collections.Generic; @@ -21,6 +20,9 @@ using System.Threading.Tasks; namespace NmeaParser.Gnss.Ntrip { + /// + /// NTRIP Client for querying an NTRIP server and opening an NTRIP stream + /// public class Client : IDisposable { private readonly string _host; @@ -30,17 +32,33 @@ namespace NmeaParser.Gnss.Ntrip private bool connected; private Task? runningTask; + /// + /// Initializes a new instance of the class + /// + /// Host name + /// Port, usually 2101 public Client(string host, int port) { _host = host; _port = port; } + /// + /// Initializes a new instance of the class + /// + /// Host name + /// Port, usually 2101 + /// Username + /// Password public Client(string host, int port, string username, string password) : this(host, port) { _auth = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(username + ":" + password)); } + /// + /// Gets a list of sources from the NTRIP endpoint + /// + /// public IEnumerable GetSourceTable() { string data = ""; @@ -77,8 +95,8 @@ namespace NmeaParser.Gnss.Ntrip { var sckt = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); sckt.Blocking = true; + sckt.ReceiveTimeout = 5000; sckt.Connect(_host, _port); - string msg = $"GET /{path} HTTP/1.1\r\n"; msg += "User-Agent: NTRIP ntripclient\r\n"; if (_auth != null) @@ -92,11 +110,29 @@ namespace NmeaParser.Gnss.Ntrip sckt.Send(data); return sckt; } - - public void Connect(string strName) + /// + /// Connects to the endpoint for the specified + /// + /// + public void Connect(NtripStream stream) { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + Connect(stream.Mountpoint); + } + + /// + /// Connects to the endpoint for the specified + /// + /// + public void Connect(string mountPoint) + { + if (mountPoint == null) + throw new ArgumentNullException(nameof(mountPoint)); + if (string.IsNullOrWhiteSpace(mountPoint)) + throw new ArgumentException(nameof(mountPoint)); if (sckt != null) throw new Exception("Connection already open"); - sckt = Request(strName); + sckt = Request(mountPoint); connected = true; runningTask = Task.Run(ReceiveThread); } @@ -104,10 +140,10 @@ namespace NmeaParser.Gnss.Ntrip private async Task ReceiveThread() { byte[] buffer = new byte[65536]; - sckt.ReceiveTimeout = 1000; + while (connected && sckt != null) { - int count = sckt.Receive(buffer, SocketFlags.None, out SocketError errorCode); + int count = sckt.Receive(buffer); if (count > 0) { DataReceived?.Invoke(this, buffer.Take(count).ToArray()); @@ -128,6 +164,10 @@ namespace NmeaParser.Gnss.Ntrip sckt = null; } + /// + /// Shuts down the stream + /// + /// public Task CloseAsync() { if (runningTask != null) @@ -137,15 +177,30 @@ namespace NmeaParser.Gnss.Ntrip runningTask = null; return t; } +#if NETSTANDARD || NETFX + return Task.FromResult(null); +#else return Task.CompletedTask; +#endif } + /// public void Dispose() { _ = CloseAsync(); } + /// + /// Fired when bytes has been received from the stream + /// public event EventHandler? DataReceived; - public event EventHandler Disconnected; + + /// + /// Fired if the socket connection was dropped, and the connection was closed. + /// + /// + /// This event is useful for handling network glitches, and trying to retry connection by calling again a few times. + /// + public event EventHandler? Disconnected; } } diff --git a/src/SampleApp.WinDesktop/Ntrip/NtripSource.cs b/src/NmeaParser/Gnss/Ntrip/NtripSource.cs similarity index 76% rename from src/SampleApp.WinDesktop/Ntrip/NtripSource.cs rename to src/NmeaParser/Gnss/Ntrip/NtripSource.cs index 8cc71da..95da81b 100644 --- a/src/SampleApp.WinDesktop/Ntrip/NtripSource.cs +++ b/src/NmeaParser/Gnss/Ntrip/NtripSource.cs @@ -14,8 +14,14 @@ namespace NmeaParser.Gnss.Ntrip { - public class NtripSource + /// + /// Baseclass for the sources returned from an NTRIP Service + /// + public abstract class NtripSource { + /// + /// Initializes a new instance of the class. + /// protected NtripSource() { } diff --git a/src/SampleApp.WinDesktop/Ntrip/NtripStream.cs b/src/NmeaParser/Gnss/Ntrip/NtripStream.cs similarity index 62% rename from src/SampleApp.WinDesktop/Ntrip/NtripStream.cs rename to src/NmeaParser/Gnss/Ntrip/NtripStream.cs index 9893109..2b2c811 100644 --- a/src/SampleApp.WinDesktop/Ntrip/NtripStream.cs +++ b/src/NmeaParser/Gnss/Ntrip/NtripStream.cs @@ -17,6 +17,9 @@ using System.Globalization; namespace NmeaParser.Gnss.Ntrip { + /// + /// Metadata on an NTRIP Data Stream + /// public class NtripStream : NtripSource { internal NtripStream(string[] d) @@ -38,15 +41,54 @@ namespace NmeaParser.Gnss.Ntrip SupportsNmea = d[11] == "1"; } + /// + /// The mountpoint used with + /// public string Mountpoint { get; } + + /// + /// Gets the unique identifier for the stream + /// public string Identifier { get; } + + /// + /// Gets the stream format + /// public string Format { get; } + + /// + /// Gets the details about the format + /// public string FormatDetails { get; } + + /// + /// Gets the wave carrier for the stream + /// public Carrier Carrier { get; } + + /// + /// Gets the network for the stream + /// public string Network { get; } + + /// + /// Gets the country code for where the stream originates + /// public string CountryCode { get; } + + /// + /// Gets the latitude location of the base station + /// public double Latitude { get; } + + /// + /// Gets the longitude location of the base station + /// public double Longitude { get; } + + /// + /// Gets a value indicating whether the stream supports NMEA + /// public bool SupportsNmea { get; } } } diff --git a/src/SampleApp.WinDesktop/GnssMonitorView.xaml b/src/SampleApp.WinDesktop/GnssMonitorView.xaml new file mode 100644 index 0000000..3366ead --- /dev/null +++ b/src/SampleApp.WinDesktop/GnssMonitorView.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs b/src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs new file mode 100644 index 0000000..6bf6837 --- /dev/null +++ b/src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using NmeaParser.Gnss; +using NmeaParser.Messages; + +namespace SampleApp.WinDesktop +{ + /// + /// Interaction logic for GnssMonitorView.xaml + /// + public partial class GnssMonitorView : UserControl + { + public GnssMonitorView() + { + InitializeComponent(); + } + + public GnssMonitor Monitor + { + get { return (GnssMonitor)GetValue(MonitorProperty); } + set { SetValue(MonitorProperty, value); } + } + + public static readonly DependencyProperty MonitorProperty = + DependencyProperty.Register(nameof(Monitor), typeof(GnssMonitor), typeof(GnssMonitorView), new PropertyMetadata(null, (d,e) => ((GnssMonitorView)d).OnMonitorPropertyChanged(e))); + + private void OnMonitorPropertyChanged(DependencyPropertyChangedEventArgs e) + { + if (e.OldValue is GnssMonitor oldMonitor) + { + oldMonitor.LocationChanged -= LocationChanged; + oldMonitor.LocationLost -= LocationChanged; + } + if (e.NewValue is GnssMonitor newMonitor) + { + newMonitor.LocationChanged += LocationChanged; + newMonitor.LocationLost += LocationChanged; + } + UpdateValues(); + } + + private void LocationChanged(object sender, System.EventArgs e) + { + Dispatcher.Invoke(UpdateValues); + } + private void UpdateValues() + { + if (Monitor == null) + Values.ItemsSource = null; + else + { + var props = Monitor.GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); + List> values = new List>(); + foreach (var prop in props.OrderBy(t => t.Name)) + { + if (prop.Name == nameof(GnssMonitor.AllMessages)) continue; + if (prop.PropertyType.IsSubclassOf(typeof(NmeaMessage))) + continue; + var value = prop.GetValue(Monitor); + if (!(value is string) && value is System.Collections.IEnumerable arr) + { + var str = "[" + string.Join(",", arr.OfType().ToArray()) + "]"; + if (str.Length == 2) + str = "[ ]"; + value = str; + } + values.Add(new KeyValuePair(prop.Name, value)); + } + Values.ItemsSource = values; + } + } + } +} diff --git a/src/SampleApp.WinDesktop/GsaControl.xaml.cs b/src/SampleApp.WinDesktop/GsaControl.xaml.cs index 0c4bd8b..3221876 100644 --- a/src/SampleApp.WinDesktop/GsaControl.xaml.cs +++ b/src/SampleApp.WinDesktop/GsaControl.xaml.cs @@ -37,7 +37,10 @@ namespace SampleApp.WinDesktop private void OnGsaPropertyChanged(DependencyPropertyChangedEventArgs e) { - vehicles.Value = string.Join(",", Message?.SatelliteIDs); + if (Message == null) + vehicles.Value = null; + else + vehicles.Value = string.Join(",", Message?.SatelliteIDs); } } } diff --git a/src/SampleApp.WinDesktop/MainWindow.xaml b/src/SampleApp.WinDesktop/MainWindow.xaml index ef5541f..fae7178 100644 --- a/src/SampleApp.WinDesktop/MainWindow.xaml +++ b/src/SampleApp.WinDesktop/MainWindow.xaml @@ -20,7 +20,7 @@ - + @@ -42,13 +42,16 @@ + + + - + messages = new Queue(101); public static NmeaParser.NmeaDevice currentDevice; + //Dialog for browsing to nmea log files private Microsoft.Win32.OpenFileDialog nmeaOpenFileDialog = new Microsoft.Win32.OpenFileDialog() { @@ -53,6 +55,7 @@ namespace SampleApp.WinDesktop if (currentDevice.IsOpen) await currentDevice.CloseAsync(); currentDevice.Dispose(); + gnssMonitorView.Monitor = null; } output.Text = ""; messages.Clear(); @@ -78,6 +81,7 @@ namespace SampleApp.WinDesktop ((NmeaParser.SerialPortDevice)device).Port.BaudRate); } await device.OpenAsync(); + gnssMonitorView.Monitor = new GnssMonitor(device); } private void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs args) diff --git a/src/SampleApp.WinDesktop/NmeaLocationDataSource.cs b/src/SampleApp.WinDesktop/NmeaLocationDataSource.cs index 0d55bb1..19a0224 100644 --- a/src/SampleApp.WinDesktop/NmeaLocationDataSource.cs +++ b/src/SampleApp.WinDesktop/NmeaLocationDataSource.cs @@ -1,120 +1,87 @@ -using Esri.ArcGISRuntime.Geometry; +// ******************************************************************************* +// * 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.Threading.Tasks; +using Esri.ArcGISRuntime.Geometry; +using NmeaParser.Gnss; namespace SampleApp.WinDesktop { public class NmeaLocationDataSource : Esri.ArcGISRuntime.Location.LocationDataSource { 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 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 + private readonly GnssMonitor m_gnssMonitor; + private readonly bool m_startStopDevice; + private double lastCourse = 0; // Course can fallback to NaN, but ArcGIS Datasource don't allow NaN course, so we cache last known as a fallback - public NmeaLocationDataSource(NmeaParser.NmeaDevice device, bool startStopDevice = true) + /// + /// Initializes a new instance of the class. + /// + /// The NMEA device to monitor + /// Whether starting this datasource also controls the underlying NMEA device + public NmeaLocationDataSource(NmeaParser.NmeaDevice device, bool startStopDevice = true) : this(new GnssMonitor(device), startStopDevice) { - if (device == null) - throw new ArgumentNullException(nameof(device)); - this.m_device = device; + } + + /// + /// Initializes a new instance of the class. + /// + /// The NMEA device to monitor + /// Whether starting this datasource also controls the underlying NMEA device + public NmeaLocationDataSource(NmeaParser.Gnss.GnssMonitor monitor, bool startStopDevice = true) + { + if (monitor == null) + throw new ArgumentNullException(nameof(monitor)); + this.m_gnssMonitor = monitor; m_startStopDevice = startStopDevice; } - void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs e) + protected async override Task OnStartAsync() { - 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) - m_supportGNMessages = true; - else if(m_supportGNMessages && message.TalkerId != NmeaParser.Talker.GlobalNavigationSatelliteSystem) - return; // If device supports combined GN* messages, ignore non-GN messages + m_gnssMonitor.LocationChanged += OnLocationChanged; + m_gnssMonitor.LocationLost += OnLocationChanged; + if (m_startStopDevice && !this.m_gnssMonitor.Device.IsOpen) + await this.m_gnssMonitor.Device.OpenAsync(); - 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.Round(Math.Sqrt(Gst.SigmaLatitudeError * Gst.SigmaLatitudeError + Gst.SigmaLongitudeError * Gst.SigmaLongitudeError), 3); - } - else if (message is NmeaParser.Messages.Rmc rmc) - { - Rmc = rmc; - if (Rmc.Active) - { - m_speed = double.IsNaN(Rmc.Speed) ? 0 : Rmc.Speed; - if (!double.IsNaN(Rmc.Course)) - m_course = Rmc.Course; - lat = Rmc.Latitude; - lon = Rmc.Longitude; - } - else - { - lostFix = true; - } - isNewFix = !m_supportGGaMessages; - } - else if (message is NmeaParser.Messages.Gga gga) - { - m_supportGGaMessages = true; - if (gga.Quality != NmeaParser.Messages.Gga.FixQuality.Invalid) - { - 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) - { - 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)); - } - } - - protected override Task OnStartAsync() - { - m_device.MessageReceived += device_MessageReceived; - if (m_startStopDevice) - return this.m_device.OpenAsync(); - else - return System.Threading.Tasks.Task.FromResult(true); + if (m_gnssMonitor.IsFixValid) + OnLocationChanged(this, EventArgs.Empty); } protected override Task OnStopAsync() { - m_device.MessageReceived -= device_MessageReceived; - m_Accuracy = double.NaN; + m_gnssMonitor.LocationChanged -= OnLocationChanged; + m_gnssMonitor.LocationLost -= OnLocationChanged; if(m_startStopDevice) - return this.m_device.CloseAsync(); + return m_gnssMonitor.Device.CloseAsync(); else - return System.Threading.Tasks.Task.FromResult(true); + return Task.CompletedTask; } - 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; } + private void OnLocationChanged(object sender, EventArgs e) + { + if (double.IsNaN(m_gnssMonitor.Longitude) || double.IsNaN(m_gnssMonitor.Latitude)) return; + if (!double.IsNaN(m_gnssMonitor.Course)) + lastCourse = m_gnssMonitor.Course; + UpdateLocation(new Esri.ArcGISRuntime.Location.Location( + timestamp: null, + position: !double.IsNaN(m_gnssMonitor.Altitude) ? new MapPoint(m_gnssMonitor.Longitude, m_gnssMonitor.Latitude, m_gnssMonitor.Altitude, wgs84_ellipsoidHeight) : new MapPoint(m_gnssMonitor.Longitude, m_gnssMonitor.Latitude, SpatialReferences.Wgs84), + horizontalAccuracy: m_gnssMonitor.HorizontalError, + verticalAccuracy: m_gnssMonitor.VerticalError, + velocity: double.IsNaN(m_gnssMonitor.Speed) ? 0 : m_gnssMonitor.Speed * 0.51444444, + course: lastCourse, + !m_gnssMonitor.IsFixValid)); + } } }