Moved NTRIP classes to NmeaParser lib and introduced new GnssMonitor class for simplified monitoring of GNSS messages

This commit is contained in:
Morten Nielsen 2020-07-29 16:45:16 -07:00
parent f3f80534f9
commit 73dbdf508f
12 changed files with 662 additions and 109 deletions

View file

@ -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
{
/// <summary>
/// Helper class for monitoring GNSS messages and combine them into a single useful location info
/// </summary>
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
/// <summary>
/// Initializes a new instance of the <see cref="GnssMonitor"/> class.
/// </summary>
/// <param name="device">The NMEA device to monitor for GNSS messages</param>
public GnssMonitor(NmeaDevice device)
{
if (device == null)
throw new ArgumentNullException(nameof(device));
Device = device;
Device.MessageReceived += NmeaMessageReceived;
}
/// <summary>
/// Gets the NMEA device that is being monitored
/// </summary>
public NmeaDevice Device { get; }
private void NmeaMessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs e)
{
OnMessageReceived(e.Message);
}
/// <summary>
/// Called when a message is received.
/// </summary>
/// <param name="message">The NMEA message that was received</param>
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);
}
}
/// <summary>
/// Gets a value indicating whether the current fix is valid.
/// </summary>
/// <remarks>
/// If <c>false</c> the provided values like <see cref="Latitude"/> and <see cref="Longitude"/> are no longer current and reflect the last known location.
/// </remarks>
/// <seealso cref="LocationLost"/>
public bool IsFixValid { get; private set; }
/// <summary>
/// Gets the latitude for the current or last known location.
/// </summary>
/// <seealso cref="IsFixValid"/>
/// <seealso cref="Longitude"/>
public double Latitude { get; private set; } = double.NaN;
/// <summary>
/// Gets the longitude for the current or last known location.
/// </summary>
/// <seealso cref="IsFixValid"/>
/// <seealso cref="Latitude"/>
public double Longitude { get; private set; } = double.NaN;
/// <summary>
/// Gets the geight above the ellipsoid
/// </summary>
public double Altitude { get; private set; } = double.NaN;
/// <summary>
/// Gets the Geoid Height. Add this value to <see cref="Altitude"/> to get the Geoid heights which is roughly MSL heights.
/// </summary>
public double GeoidHeight { get; private set; } = double.NaN;
/// <summary>
/// Gets the speed in knots
/// </summary>
public double Speed => Rmc?.Speed ?? Vtg?.SpeedKnots ?? double.NaN;
/// <summary>
/// Gets the current cource
/// </summary>
public double Course => Rmc?.Course ?? double.NaN;
/// <summary>
/// Gets an estimate of the horizontal error in meters
/// </summary>
public double HorizontalError { get; private set; } = double.NaN;
/// <summary>
/// Gets an estimate of the vertical error in meters
/// </summary>
public double VerticalError { get; private set; } = double.NaN;
/// <summary>
/// Gets the horizontal dilution of precision
/// </summary>
public double Hdop => Gsa?.Hdop ?? Gga?.Hdop ?? double.NaN;
/// <summary>
/// Gets the 3D point dilution of precision
/// </summary>
public double Pdop => Gsa?.Pdop ?? double.NaN;
/// <summary>
/// Gets the vertical dilution of precision
/// </summary>
public double Vdop => Gsa?.Vdop ?? double.NaN;
/// <summary>
/// Gets the latest known GSA message.
/// </summary>
public Gsa? Gsa { get; private set; }
/// <summary>
/// Gets the latest known GGA message.
/// </summary>
public Gga? Gga { get; private set; }
/// <summary>
/// Gets the latest known RMC message.
/// </summary>
public Rmc? Rmc { get; private set; }
/// <summary>
/// Gets the latest known GST message.
/// </summary>
public Gst? Gst { get; private set; }
/// <summary>
/// Gets the latest known DTM message.
/// </summary>
public Dtm? Dtm { get; private set; }
/// <summary>
/// Gets the latest known VTG message.
/// </summary>
public Vtg? Vtg { get; private set; }
/// <summary>
/// Gets the current fix time
/// </summary>
public TimeSpan FixTime { get; private set; }
/// <summary>
/// Gets a list of satellite vehicles in the sky
/// </summary>
public IEnumerable<SatelliteVehicle> Satellites => AllMessages.Values.OfType<Gsv>().SelectMany(s => s.SVs);
/// <summary>
/// Gets the number of satellites in the sky
/// </summary>
public int SatellitesInView => AllMessages.Values.OfType<Gsv>().Sum(s => s.SatellitesInView);
/// <summary>
/// Gets the quality of the current fix
/// </summary>
public Gga.FixQuality FixQuality => !IsFixValid ? Gga.FixQuality.Invalid : (Gga?.Quality ?? Gga.FixQuality.GpsFix);
/// <summary>
/// Gets a list of all NMEA messages currently part of this location
/// </summary>
public Dictionary<string, NmeaMessage> AllMessages { get; } = new Dictionary<string, NmeaMessage>();
/// <summary>
/// Gets a value indicating the current Datum being used.
/// </summary>
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;
}
}
}
/// <summary>
/// Raised when a new location has been updated
/// </summary>
public event EventHandler? LocationChanged;
/// <summary>
/// Raised if location tracking was lost
/// </summary>
/// <seealso cref="IsFixValid"/>
public event EventHandler? LocationLost;
}
}

View file

@ -14,10 +14,24 @@
namespace NmeaParser.Gnss.Ntrip
{
/// <summary>
/// Enumeration for the carrier used by the <see cref="NtripStream"/>
/// </summary>
public enum Carrier : int
{
No = 0,
/// <summary>
/// None / unknown
/// </summary>
None = 0,
/// <summary>
/// L1 wave
/// </summary>
L1 = 1,
/// <summary>
/// L1 and L2 waves
/// </summary>
L1L2 = 2
}
}

View file

@ -17,6 +17,9 @@ using System.Net;
namespace NmeaParser.Gnss.Ntrip
{
/// <summary>
/// Gets metadata about the NTRIP Caster
/// </summary>
public class Caster : NtripSource
{
internal Caster (string[] d)
@ -33,6 +36,18 @@ namespace NmeaParser.Gnss.Ntrip
FallbackAddress = IPAddress.Parse(d[9]);
}
/// <summary>
/// Initializes a new instance of the <see cref="Caster"/> class.
/// </summary>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="identifier"></param>
/// <param name="_operator"></param>
/// <param name="supportsNmea"></param>
/// <param name="countryCode"></param>
/// <param name="latitude"></param>
/// <param name="longitude"></param>
/// <param name="fallbackkAddress"></param>
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;
}
/// <summary>
/// Gets the caster IP Address
/// </summary>
public IPAddress Address { get; }
/// <summary>
/// Gets the caster port
/// </summary>
public int Port { get; }
/// <summary>
/// Gets the caster identifier
/// </summary>
public string Identifier { get; }
/// <summary>
/// Gets the caster operator
/// </summary>
public string Operator { get; }
/// <summary>
/// Gets a value indicating whether it supports NMEA
/// </summary>
public bool SupportsNmea { get; }
/// <summary>
/// Gets the country code for the caster origin
/// </summary>
public string CountryCode { get; }
/// <summary>
/// Gets the latitude for the caster
/// </summary>
public double Latitude { get; }
/// <summary>
/// Gets the longitude for the caster
/// </summary>
public double Longitude { get; }
/// <summary>
/// Gets the fallback address for the caster
/// </summary>
public IPAddress FallbackAddress { get; }
}
}

View file

@ -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
{
/// <summary>
/// NTRIP Client for querying an NTRIP server and opening an NTRIP stream
/// </summary>
public class Client : IDisposable
{
private readonly string _host;
@ -30,17 +32,33 @@ namespace NmeaParser.Gnss.Ntrip
private bool connected;
private Task? runningTask;
/// <summary>
/// Initializes a new instance of the <see cref="Client"/> class
/// </summary>
/// <param name="host">Host name</param>
/// <param name="port">Port, usually 2101</param>
public Client(string host, int port)
{
_host = host;
_port = port;
}
/// <summary>
/// Initializes a new instance of the <see cref="Client"/> class
/// </summary>
/// <param name="host">Host name</param>
/// <param name="port">Port, usually 2101</param>
/// <param name="username">Username</param>
/// <param name="password">Password</param>
public Client(string host, int port, string username, string password) : this(host, port)
{
_auth = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(username + ":" + password));
}
/// <summary>
/// Gets a list of sources from the NTRIP endpoint
/// </summary>
/// <returns></returns>
public IEnumerable<NtripSource> 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)
/// <summary>
/// Connects to the endpoint for the specified <see cref="NtripStream.Mountpoint"/>
/// </summary>
/// <param name="stream"></param>
public void Connect(NtripStream stream)
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
Connect(stream.Mountpoint);
}
/// <summary>
/// Connects to the endpoint for the specified <see cref="NtripStream.Mountpoint"/>
/// </summary>
/// <param name="mountPoint"></param>
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;
}
/// <summary>
/// Shuts down the stream
/// </summary>
/// <returns></returns>
public Task CloseAsync()
{
if (runningTask != null)
@ -137,15 +177,30 @@ namespace NmeaParser.Gnss.Ntrip
runningTask = null;
return t;
}
#if NETSTANDARD || NETFX
return Task.FromResult<object?>(null);
#else
return Task.CompletedTask;
#endif
}
/// <inheritdoc />
public void Dispose()
{
_ = CloseAsync();
}
/// <summary>
/// Fired when bytes has been received from the stream
/// </summary>
public event EventHandler<byte[]>? DataReceived;
public event EventHandler Disconnected;
/// <summary>
/// Fired if the socket connection was dropped, and the connection was closed.
/// </summary>
/// <remarks>
/// This event is useful for handling network glitches, and trying to retry connection by calling <see cref="Connect(string)"/> again a few times.
/// </remarks>
public event EventHandler? Disconnected;
}
}

View file

@ -14,8 +14,14 @@
namespace NmeaParser.Gnss.Ntrip
{
public class NtripSource
/// <summary>
/// Baseclass for the sources returned from an NTRIP Service
/// </summary>
public abstract class NtripSource
{
/// <summary>
/// Initializes a new instance of the <see cref="NtripSource"/> class.
/// </summary>
protected NtripSource()
{
}

View file

@ -17,6 +17,9 @@ using System.Globalization;
namespace NmeaParser.Gnss.Ntrip
{
/// <summary>
/// Metadata on an NTRIP Data Stream
/// </summary>
public class NtripStream : NtripSource
{
internal NtripStream(string[] d)
@ -38,15 +41,54 @@ namespace NmeaParser.Gnss.Ntrip
SupportsNmea = d[11] == "1";
}
/// <summary>
/// The mountpoint used with <see cref="Client.Connect(string)"/>
/// </summary>
public string Mountpoint { get; }
/// <summary>
/// Gets the unique identifier for the stream
/// </summary>
public string Identifier { get; }
/// <summary>
/// Gets the stream format
/// </summary>
public string Format { get; }
/// <summary>
/// Gets the details about the format
/// </summary>
public string FormatDetails { get; }
/// <summary>
/// Gets the wave carrier for the stream
/// </summary>
public Carrier Carrier { get; }
/// <summary>
/// Gets the network for the stream
/// </summary>
public string Network { get; }
/// <summary>
/// Gets the country code for where the stream originates
/// </summary>
public string CountryCode { get; }
/// <summary>
/// Gets the latitude location of the base station
/// </summary>
public double Latitude { get; }
/// <summary>
/// Gets the longitude location of the base station
/// </summary>
public double Longitude { get; }
/// <summary>
/// Gets a value indicating whether the stream supports NMEA
/// </summary>
public bool SupportsNmea { get; }
}
}

View file

@ -0,0 +1,28 @@
<UserControl x:Class="SampleApp.WinDesktop.GnssMonitorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SampleApp.WinDesktop"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel DataContext="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=Message}">
<Border Background="LightGray" Padding="10" Margin="-10,-10,-10,5" x:Name="HeaderPanel">
<TextBlock Text="{Binding MessageType}" x:Name="typeName" FontSize="20" FontWeight="Bold" Foreground="White" />
</Border>
<ItemsControl x:Name="Values">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="100" Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Key}" Margin="0,0,5,0" VerticalAlignment="Top" />
<TextBlock Text="{Binding Value}" TextWrapping="Wrap" Grid.Column="1" VerticalAlignment="Top" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View file

@ -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
{
/// <summary>
/// Interaction logic for GnssMonitorView.xaml
/// </summary>
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<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>();
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<object>().ToArray()) + "]";
if (str.Length == 2)
str = "[ ]";
value = str;
}
values.Add(new KeyValuePair<string, object>(prop.Name, value));
}
Values.ItemsSource = values;
}
}
}
}

View file

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

View file

@ -20,7 +20,7 @@
</Window.Resources>
<Grid>
<TabControl>
<TabItem Header="GPS Info">
<TabItem Header="Messages">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" Background="#FFEEEEEE">
<WrapPanel x:Name="MessagePanel">
<local:RmcControl x:Name="gprmcView" Style="{StaticResource card}" Visibility="{Binding Message, ElementName=gprmcView, Converter={StaticResource nullConv}}" />
@ -42,13 +42,16 @@
<local:SatelliteSnr Grid.Row="1" x:Name="satSnr" />
</Grid>
</TabItem>
<TabItem Header="GNSS Monitor">
<local:GnssMonitorView x:Name="gnssMonitorView" />
</TabItem>
<TabItem Header="Map">
<local:View2D x:Name="view2d" />
</TabItem>
<TabItem Header="3D">
<local:View3D x:Name="view3d" />
</TabItem>
<TabItem Header="Messages">
<TabItem Header="NMEA Log">
<TextBox x:Name="output"
AcceptsReturn="True"
IsReadOnly="True"

View file

@ -1,4 +1,5 @@
using System;
using NmeaParser.Gnss;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -15,6 +16,7 @@ namespace SampleApp.WinDesktop
{
private Queue<string> messages = new Queue<string>(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)

View file

@ -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)
/// <summary>
/// Initializes a new instance of the <see cref="NmeaLocationDataSource"/> class.
/// </summary>
/// <param name="device">The NMEA device to monitor</param>
/// <param name="startStopDevice">Whether starting this datasource also controls the underlying NMEA device</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="NmeaLocationDataSource"/> class.
/// </summary>
/// <param name="monitor">The NMEA device to monitor</param>
/// <param name="startStopDevice">Whether starting this datasource also controls the underlying NMEA device</param>
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<bool>.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<bool>.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));
}
}
}