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

View file

@ -1,23 +0,0 @@
// *******************************************************************************
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// ******************************************************************************
namespace NmeaParser.Gnss.Ntrip
{
public enum Carrier : int
{
No = 0,
L1 = 1,
L1L2 = 2
}
}

View file

@ -1,59 +0,0 @@
// *******************************************************************************
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// ******************************************************************************
using System.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; }
}
}

View file

@ -1,151 +0,0 @@
// *******************************************************************************
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// ******************************************************************************
#nullable enable
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<NtripSource> 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<NtripSource> sources = new List<NtripSource>();
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];
sckt.ReceiveTimeout = 1000;
while (connected && sckt != null)
{
int count = sckt.Receive(buffer, SocketFlags.None, out SocketError errorCode);
if (count > 0)
{
DataReceived?.Invoke(this, buffer.Take(count).ToArray());
}
await Task.Yield();
if (!sckt.Connected)
{
if (connected)
{
connected = false;
Disconnected?.Invoke(this, EventArgs.Empty);
}
break;
}
}
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<byte[]>? DataReceived;
public event EventHandler Disconnected;
}
}

View file

@ -1,23 +0,0 @@
// *******************************************************************************
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// ******************************************************************************
namespace NmeaParser.Gnss.Ntrip
{
public class NtripSource
{
protected NtripSource()
{
}
}
}

View file

@ -1,52 +0,0 @@
// *******************************************************************************
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// ******************************************************************************
using System;
using System.Globalization;
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; }
}
}