mirror of
https://github.com/dotMorten/NmeaParser.git
synced 2026-01-29 11:54:40 +01:00
Merge remote-tracking branch 'origin/main' into docfx_monikers
This commit is contained in:
commit
8583c54017
|
|
@ -8,50 +8,185 @@ 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<LocationInfo> 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 = rmc.Course,
|
||||
Speed = 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();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom location class with the additional NMEA information associated with it
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BufferedStreamDevice"/> class.
|
||||
|
|
@ -39,10 +42,10 @@ namespace NmeaParser
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BufferedStreamDevice"/> class.
|
||||
/// </summary>
|
||||
/// <param name="readSpeed">The time to wait between each group of lines being read in milliseconds</param>
|
||||
protected BufferedStreamDevice(int readSpeed)
|
||||
/// <param name="burstRate">The time to wait between each group of lines being read in milliseconds</param>
|
||||
protected BufferedStreamDevice(int burstRate)
|
||||
{
|
||||
m_readSpeed = readSpeed;
|
||||
BurstRate = TimeSpan.FromMilliseconds(burstRate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -55,63 +58,172 @@ namespace NmeaParser
|
|||
/// <inheritdoc />
|
||||
protected sealed async override Task<System.IO.Stream> 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);
|
||||
m_stream.EndOfStreamReached += OnEndOfStreamReached;
|
||||
return m_stream;
|
||||
}
|
||||
|
||||
private void OnEndOfStreamReached(object sender, EventArgs e)
|
||||
{
|
||||
EndOfStreamReached?.Invoke(this, e);
|
||||
if (m_stream is BufferedStream stream && !stream.CanRewind && IsOpen)
|
||||
{
|
||||
// If we can't rewind the stream, stop
|
||||
_ = CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the emulated baud rate. Defaults to 115200
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that if the baud rate gets very low, while keeping a high <see cref="BurstRate"/>, 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.
|
||||
/// </remarks>
|
||||
public uint EmulatedBaudRate
|
||||
{
|
||||
get => emulationSettings.EmulatedBaudRate;
|
||||
set => emulationSettings.EmulatedBaudRate = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the emulated burst rate - that is the frequency of each burst of messages. Defaults to 1 second (1hz).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that if the burst rate gets very high, while keeping a low <see cref="EmulatedBaudRate"/>, 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.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the separator between each burst of data. Defaults to <see cref="BurstEmulationSeparator.FirstToken"/>.
|
||||
/// </summary>
|
||||
public BurstEmulationSeparator BurstSeparator
|
||||
{
|
||||
get => emulationSettings.Separator;
|
||||
set => emulationSettings.Separator = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task CloseStreamAsync(System.IO.Stream stream)
|
||||
{
|
||||
m_stream?.Dispose();
|
||||
if (m_stream != null)
|
||||
{
|
||||
m_stream.EndOfStreamReached -= OnEndOfStreamReached;
|
||||
m_stream?.Dispose();
|
||||
}
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defined how a burst of data is separated
|
||||
/// </summary>
|
||||
/// <seealso cref="BufferedStreamDevice.BurstSeparator"/>
|
||||
public enum BurstEmulationSeparator
|
||||
{
|
||||
/// <summary>
|
||||
/// The first NMEA token encountered will be used as an indicator for pauses between bursts
|
||||
/// </summary>
|
||||
FirstToken,
|
||||
/// <summary>
|
||||
/// An empty line in the NMEA stream should indicate a pause in the burst of messages
|
||||
/// </summary>
|
||||
EmptyLine
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the stream has reached the end. If the stream can be revound, it'll start over, unless you stop the device in this thread.
|
||||
/// </summary>
|
||||
public event EventHandler? EndOfStreamReached;
|
||||
|
||||
// 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 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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BufferedStream"/> class.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="readSpeed">The read speed in milliseconds.</param>
|
||||
public BufferedStream(StreamReader stream, int readSpeed)
|
||||
/// <param name="settings">Emulation settings.</param>
|
||||
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)
|
||||
|
||||
internal bool CanRewind => m_sr.BaseStream.CanSeek;
|
||||
|
||||
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
|
||||
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 (!lastLineRead.StartsWith(groupToken, StringComparison.Ordinal)) //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)
|
||||
|
|
@ -121,37 +233,54 @@ 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()
|
||||
|
||||
private string? ReadLine()
|
||||
{
|
||||
if (m_tcs.IsCancellationRequested)
|
||||
return null;
|
||||
if (m_sr.EndOfStream)
|
||||
{
|
||||
EndOfStreamReached?.Invoke(this, EventArgs.Empty);
|
||||
if (m_tcs.IsCancellationRequested)
|
||||
return null;
|
||||
m_sr.BaseStream.Seek(0, SeekOrigin.Begin); //start over
|
||||
}
|
||||
return m_sr.ReadLine() + '\n';
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRead { get { return true; } }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanSeek { get { return false; } }
|
||||
public override bool CanRead => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanWrite { get { return false; } }
|
||||
public override bool CanSeek => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanWrite => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Flush() { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Length { get { return m_sr.BaseStream.Length; } }
|
||||
public override long Length => m_sr.BaseStream.Length;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Position
|
||||
{
|
||||
get { return m_sr.BaseStream.Position; }
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
get => m_sr.BaseStream.Position;
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -178,30 +307,23 @@ namespace NmeaParser
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
m_tcs.Cancel();
|
||||
m_sr.Dispose();
|
||||
m_timer.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
internal event EventHandler? EndOfStreamReached;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
305
src/NmeaParser/Gnss/GnssMonitor.cs
Normal file
305
src/NmeaParser/Gnss/GnssMonitor.cs
Normal 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 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;
|
||||
FixTime = Rmc.FixTime.TimeOfDay;
|
||||
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;
|
||||
}
|
||||
FixTime = Gga.FixTime;
|
||||
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;
|
||||
}
|
||||
}
|
||||
37
src/NmeaParser/Gnss/Ntrip/Carrier.cs
Normal file
37
src/NmeaParser/Gnss/Ntrip/Carrier.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// *******************************************************************************
|
||||
// * 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumeration for the carrier used by the <see cref="NtripStream"/>
|
||||
/// </summary>
|
||||
public enum Carrier : int
|
||||
{
|
||||
/// <summary>
|
||||
/// None / unknown
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// L1 wave
|
||||
/// </summary>
|
||||
L1 = 1,
|
||||
|
||||
/// <summary>
|
||||
/// L1 and L2 waves
|
||||
/// </summary>
|
||||
L1L2 = 2
|
||||
}
|
||||
}
|
||||
109
src/NmeaParser/Gnss/Ntrip/Caster.cs
Normal file
109
src/NmeaParser/Gnss/Ntrip/Caster.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// *******************************************************************************
|
||||
// * 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets metadata about the NTRIP Caster
|
||||
/// </summary>
|
||||
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]);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
Port = port;
|
||||
Identifier = identifier;
|
||||
Operator = _operator;
|
||||
SupportsNmea = supportsNmea;
|
||||
CountryCode = countryCode;
|
||||
Latitude = latitude;
|
||||
Longitude = longitude;
|
||||
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; }
|
||||
}
|
||||
}
|
||||
225
src/NmeaParser/Gnss/Ntrip/Client.cs
Normal file
225
src/NmeaParser/Gnss/Ntrip/Client.cs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
// *******************************************************************************
|
||||
// * 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.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
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
|
||||
{
|
||||
private readonly string _host;
|
||||
private readonly int _port;
|
||||
private string? _auth;
|
||||
|
||||
/// <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 = "";
|
||||
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.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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the endpoint for the specified <see cref="NtripStream.Mountpoint"/>
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
public Stream OpenStream(NtripStream stream)
|
||||
{
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
return OpenStream(stream.Mountpoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the endpoint for the specified <see cref="NtripStream.Mountpoint"/>
|
||||
/// </summary>
|
||||
/// <param name="mountPoint"></param>
|
||||
public Stream OpenStream(string mountPoint)
|
||||
{
|
||||
if (mountPoint == null)
|
||||
throw new ArgumentNullException(nameof(mountPoint));
|
||||
if (string.IsNullOrWhiteSpace(mountPoint))
|
||||
throw new ArgumentException(nameof(mountPoint));
|
||||
|
||||
return new NtripDataStream(() => Request(mountPoint));
|
||||
}
|
||||
|
||||
private class NtripDataStream : System.IO.Stream
|
||||
{
|
||||
private Func<Socket> m_openSocketAction;
|
||||
private Socket m_socket;
|
||||
|
||||
public NtripDataStream(Func<Socket> openSocketAction)
|
||||
{
|
||||
m_openSocketAction = openSocketAction;
|
||||
m_socket = openSocketAction();
|
||||
}
|
||||
|
||||
public override bool CanRead => m_socket.Connected;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => -1;
|
||||
|
||||
long position = 0;
|
||||
public override long Position { get => position; set => throw new NotSupportedException(); }
|
||||
|
||||
public override void Flush() => throw new NotSupportedException();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (isDiposed)
|
||||
throw new ObjectDisposedException("NTRIP Stream");
|
||||
if(!m_socket.Connected)
|
||||
{
|
||||
// reconnect
|
||||
m_socket.Dispose();
|
||||
m_socket = m_openSocketAction();
|
||||
}
|
||||
int read = m_socket.Receive(buffer, offset, count, SocketFlags.None);
|
||||
position += read;
|
||||
return read;
|
||||
}
|
||||
#if !NETSTANDARD1_4
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
cancellationToken.Register(() => tcs.TrySetCanceled());
|
||||
if (isDiposed)
|
||||
throw new ObjectDisposedException("NTRIP Stream");
|
||||
if (!m_socket.Connected)
|
||||
{
|
||||
// reconnect
|
||||
m_socket.Dispose();
|
||||
m_socket = m_openSocketAction();
|
||||
}
|
||||
m_socket.BeginReceive(buffer, offset, count, SocketFlags.None, ReceiveCallback, tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private void ReceiveCallback(IAsyncResult ar)
|
||||
{
|
||||
TaskCompletionSource<int> tcs = (TaskCompletionSource<int>)ar.AsyncState;
|
||||
if (tcs.Task.IsCanceled) return;
|
||||
try
|
||||
{
|
||||
int bytesRead = m_socket.EndReceive(ar);
|
||||
position += bytesRead;
|
||||
tcs.TrySetResult(bytesRead);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
public override long Seek(long offset, SeekOrigin origin) => 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();
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
isDiposed = true;
|
||||
m_socket.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
private bool isDiposed;
|
||||
public override int ReadTimeout { get => m_socket.ReceiveTimeout; set => m_socket.ReceiveTimeout = value; }
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/NmeaParser/Gnss/Ntrip/NtripSource.cs
Normal file
29
src/NmeaParser/Gnss/Ntrip/NtripSource.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// *******************************************************************************
|
||||
// * 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
|
||||
{
|
||||
/// <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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/NmeaParser/Gnss/Ntrip/NtripStream.cs
Normal file
94
src/NmeaParser/Gnss/Ntrip/NtripStream.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// *******************************************************************************
|
||||
// * 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata on an NTRIP Data Stream
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The mountpoint used with <see cref="Client.OpenStream(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; }
|
||||
}
|
||||
}
|
||||
149
src/NmeaParser/Nmea/Dtm.cs
Normal file
149
src/NmeaParser/Nmea/Dtm.cs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
// *******************************************************************************
|
||||
// * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// * you may not use this file except in compliance with the License.
|
||||
// * You may obtain a copy of the License at
|
||||
// *
|
||||
// * http://www.apache.org/licenses/LICENSE-2.0
|
||||
// *
|
||||
// * Unless required by applicable law or agreed to in writing, software
|
||||
// * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// * See the License for the specific language governing permissions and
|
||||
// * limitations under the License.
|
||||
// ******************************************************************************
|
||||
|
||||
using System;
|
||||
|
||||
namespace NmeaParser.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Local geodetic datum and datum offsets from a reference datum.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This sentence is used to define the datum to which a position location and geographic
|
||||
/// locations in subsequent sentences, is referenced. Latitude, longitude and altitude offsets
|
||||
/// from the reference datum, and the selection of reference datum, are also provided.</para>
|
||||
/// <para>
|
||||
/// The datum sentence should be transmitted immediately prior to every positional sentence (e.g., <c>GLL</c>,
|
||||
/// <c>BWC</c>, <c>WPL</c>) that is referenced to a datum other than WGS84, which is the datum recommended by IMO.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For all datums the DTM sentence should be transmitted prior to any datum change and periodically at
|
||||
/// intervals of not greater than 30 seconds.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dtm")]
|
||||
[NmeaMessageType("--DTM")]
|
||||
public class Dtm : NmeaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Gll"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The message type</param>
|
||||
/// <param name="message">The NMEA message values.</param>
|
||||
public Dtm (string type, string[] message) : base(type, message)
|
||||
{
|
||||
if (message == null || message.Length < 8)
|
||||
throw new ArgumentException("Invalid DTM", "message");
|
||||
LocalDatumCode = message[0];
|
||||
if (message[1].Length > 0)
|
||||
LocalDatumSubdivisionCode = message[1][0];
|
||||
LatitudeOffset = NmeaMessage.StringToDouble(message[2]) * (message[3]=="S" ? -1 : 1);
|
||||
LongitudeOffset = NmeaMessage.StringToDouble(message[4]) * (message[5] == "W" ? -1 : 1);
|
||||
AltitudeOffset = NmeaMessage.StringToDouble(message[6]);
|
||||
ReferenceDatumCode = message[7];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Local datum code
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Three character alpha code for local datum. If not one of the listed earth-centered datums, or <c>999</c>
|
||||
/// for user defined datum, use IHO datum code from International Hydrographic Organization Publication S-60
|
||||
/// Appendices B and C. String.Empty if unknown.</para>
|
||||
/// <para>
|
||||
/// Users should be aware that chart transformations based on IHO S60 parameters may result in significant
|
||||
/// positional errors when applied to chart data.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Common known datum codes are:
|
||||
/// <table>
|
||||
/// <tr>
|
||||
/// <th>Code</th>
|
||||
/// <th>Datum</th>
|
||||
/// </tr>
|
||||
/// <tr><td><c>W84</c></td><td>WGS 84</td></tr>
|
||||
/// <tr><td><c>W72</c></td><td>WGS 72</td></tr>
|
||||
/// <tr><td><c>S85</c></td><td>SGS 85</td></tr>
|
||||
/// <tr><td><c>P90</c></td><td>PE 90</td></tr>
|
||||
/// <tr><td><c>999</c></td><td>User Defined</td></tr>
|
||||
/// <tr><td><c>Others</c></td><td>IHO Datum Code</td></tr>
|
||||
/// </table>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public string LocalDatumCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Local datum subdivision code.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// One character subdivision datum code when available or user defined reference character
|
||||
/// for user defined datums, null field otherwise. Subdivision character from IHO Publication S-60
|
||||
/// Appendices B and C.
|
||||
/// </remarks>
|
||||
public char? LocalDatumSubdivisionCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Latitude Offset, decimal degrees
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Latitude and longitude offsets are positive numbers, the altitude offset may be negative. Offsets
|
||||
/// change with position; position in the local datum is offset from the position in the reference datum in the directions
|
||||
/// indicated:
|
||||
/// <c>P_local_datum = P_ref_datum + offset</c>
|
||||
/// </remarks>
|
||||
public double LatitudeOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Longitude Offset in minutes
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Latitude and longitude offsets are positive numbers, the altitude offset may be negative. Offsets
|
||||
/// change with position; position in the local datum is offset from the position in the reference datum in the directions
|
||||
/// indicated:
|
||||
/// <c>P_local_datum = P_ref_datum + offset</c>
|
||||
/// </remarks>
|
||||
public double LongitudeOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Altitude Offset in minutes
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Latitude and longitude offsets are positive numbers, the altitude offset may be negative. Offsets
|
||||
/// change with position; position in the local datum is offset from the position in the reference datum in the directions
|
||||
/// indicated:
|
||||
/// <c>P_local_datum = P_ref_datum + offset</c>
|
||||
/// </remarks>
|
||||
public double AltitudeOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference datum code
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Common known datum codes are:
|
||||
/// <table>
|
||||
/// <tr>
|
||||
/// <th>Code</th>
|
||||
/// <th>Datum</th>
|
||||
/// </tr>
|
||||
/// <tr><td><c>W84</c></td><td>WGS 84</td></tr>
|
||||
/// <tr><td><c>W72</c></td><td>WGS 72</td></tr>
|
||||
/// <tr><td><c>S85</c></td><td>SGS 85</td></tr>
|
||||
/// <tr><td><c>P90</c></td><td>PE 90</td></tr>
|
||||
/// </table>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public string ReferenceDatumCode { get; }
|
||||
}
|
||||
}
|
||||
135
src/NmeaParser/Nmea/Gbs.cs
Normal file
135
src/NmeaParser/Nmea/Gbs.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// *******************************************************************************
|
||||
// * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// * you may not use this file except in compliance with the License.
|
||||
// * You may obtain a copy of the License at
|
||||
// *
|
||||
// * http://www.apache.org/licenses/LICENSE-2.0
|
||||
// *
|
||||
// * Unless required by applicable law or agreed to in writing, software
|
||||
// * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// * See the License for the specific language governing permissions and
|
||||
// * limitations under the License.
|
||||
// ******************************************************************************
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace NmeaParser.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// GNSS Satellite Fault Detection
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This sentence is used to support Receiver Autonomous Integrity Monitoring (RAIM). Given that a GNSS
|
||||
/// receiver is tracking enough satellites to perform integrity checks of the positioning quality of the position
|
||||
/// solution a sentence is needed to report the output of this process to other systems to advise the system
|
||||
/// user.With the RAIM in the GNSS receiver, the receiver can isolate faults to individual satellites and not
|
||||
/// use them in its position and velocity calculations.Also, the GNSS receiver can still track the satellite and
|
||||
/// easily judge when it is back within tolerance.This sentence shall be used for reporting this RAIM
|
||||
/// information. To perform this integrity function, the GNSS receiver must have at least two observables in
|
||||
/// addition to the minimum required for navigation.Normally these observables take the form of additional
|
||||
/// redundant satellites.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If only GPS, GLONASS, Galileo, BDS, QZSS, NavIC (IRNSS) is used for the reported position solution
|
||||
/// the talker ID is GP, GL, GA, GB, GQ, GI respectively and the errors pertain to the individual system.If
|
||||
/// satellites from multiple systems are used to obtain the reported position solution the talker ID is GN and
|
||||
/// the errors pertain to the combined solution.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dtm")]
|
||||
[NmeaMessageType("--GBS")]
|
||||
public class Gbs : NmeaMessage, ITimestampedMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Gbs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The message type</param>
|
||||
/// <param name="message">The NMEA message values.</param>
|
||||
public Gbs (string type, string[] message) : base(type, message)
|
||||
{
|
||||
if (message == null || message.Length < 8)
|
||||
throw new ArgumentException("Invalid GBS", "message");
|
||||
FixTime = StringToTimeSpan(message[0]);
|
||||
LatitudeError = NmeaMessage.StringToDouble(message[1]);
|
||||
LongitudeError = NmeaMessage.StringToDouble(message[2]);
|
||||
AltitudeError = NmeaMessage.StringToDouble(message[3]);
|
||||
if (int.TryParse(message[4], System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out int id))
|
||||
SatelliteId = id;
|
||||
MissedDetectionProbability = NmeaMessage.StringToDouble(message[5]);
|
||||
BiasEstimate = NmeaMessage.StringToDouble(message[6]);
|
||||
StandardDeviation = NmeaMessage.StringToDouble(message[7]);
|
||||
}
|
||||
/// <summary>
|
||||
/// UTC time of the GGA or GNS fix associated with this sentence.
|
||||
/// </summary>
|
||||
public TimeSpan FixTime { get; }
|
||||
|
||||
TimeSpan ITimestampedMessage.Timestamp => FixTime;
|
||||
|
||||
/// <summary>
|
||||
/// Expected Error in latitude
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Expected error in meters due to bias, with noise = 0
|
||||
/// </remarks>
|
||||
public double LatitudeError { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected Error in longitude
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Expected error in meters due to bias, with noise = 0
|
||||
/// </remarks>
|
||||
public double LongitudeError { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected Error in altitude
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Expected error in meters due to bias, with noise = 0
|
||||
/// </remarks>
|
||||
public double AltitudeError { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ID number of most likely failed satellite
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Satellite ID numbers. To avoid possible confusion caused by repetition of satellite ID numbers when using
|
||||
/// multiple satellite systems, the following convention has been adopted:
|
||||
/// <ul>
|
||||
/// <li>a) GPS satellites are identified by their PRN numbers, which range from 1 to 32.</li>
|
||||
/// <li>b) The numbers 33-64 are reserved for SBAS satellites. The SBAS system PRN numbers are 120-138.
|
||||
/// The offset from NMEA SBAS SV ID to SBAS PRN number is 87. A SBAS PRN number of 120
|
||||
/// minus 87 yields the SV ID of 33. The addition of 87 to the SV ID yields the SBAS PRN number.</li>
|
||||
/// <li>c) The numbers 65-96 are reserved for GLONASS satellites. GLONASS satellites are identified by
|
||||
/// 64+satellite slot number.The slot numbers are 1 through 24 for the full GLONASS constellation
|
||||
/// of 24 satellites, this gives a range of 65 through 88. The numbers 89 through 96 are available if
|
||||
/// slot numbers above 24 are allocated to on-orbit spares.
|
||||
/// </li>
|
||||
/// <li>See Note 3 for other GNSS not listed in a), b), or c) above to determine meaning of satellite ID when Talker ID GN is used</li>
|
||||
/// </ul>
|
||||
/// </para>
|
||||
///
|
||||
/// </remarks>
|
||||
public int? SatelliteId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Probability of missed detection for most likely failed satellite
|
||||
/// </summary>
|
||||
public double MissedDetectionProbability { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Estimate of bias in meters on most likely failed satellite
|
||||
/// </summary>
|
||||
public double BiasEstimate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Standard deviation of bias estimate
|
||||
/// </summary>
|
||||
public double StandardDeviation { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ namespace NmeaParser.Messages
|
|||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gpgga")]
|
||||
[NmeaMessageType("--GGA")]
|
||||
public class Gga : NmeaMessage
|
||||
public class Gga : NmeaMessage, ITimestampedMessage, IGeographicLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Gga"/> class.
|
||||
|
|
@ -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,13 +116,15 @@ namespace NmeaParser.Messages
|
|||
/// <summary>
|
||||
/// Time since last DGPS update (ie age of the differential GPS data)
|
||||
/// </summary>
|
||||
public TimeSpan TimeSinceLastDgpsUpdate { get; }
|
||||
public TimeSpan? TimeSinceLastDgpsUpdate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Differential Reference Station ID
|
||||
/// </summary>
|
||||
public int DgpsStationId { get; }
|
||||
|
||||
TimeSpan ITimestampedMessage.Timestamp => FixTime;
|
||||
|
||||
/// <summary>
|
||||
/// Fix quality indicater
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ namespace NmeaParser.Messages
|
|||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gll")]
|
||||
[NmeaMessageType("--GLL")]
|
||||
public class Gll : NmeaMessage
|
||||
public class Gll : NmeaMessage, ITimestampedMessage, IGeographicLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Gll"/> class.
|
||||
|
|
@ -85,6 +85,8 @@ namespace NmeaParser.Messages
|
|||
/// </summary>
|
||||
public Mode ModeIndicator { get; }
|
||||
|
||||
TimeSpan ITimestampedMessage.Timestamp => FixTime;
|
||||
|
||||
/// <summary>
|
||||
/// Positioning system Mode Indicator
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ namespace NmeaParser.Messages
|
|||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gns")]
|
||||
[NmeaMessageType("--GNS")]
|
||||
public class Gns : NmeaMessage
|
||||
public class Gns : NmeaMessage, ITimestampedMessage, IGeographicLocation
|
||||
{
|
||||
/*
|
||||
* Example of GNS messages:
|
||||
|
|
@ -149,13 +149,13 @@ namespace NmeaParser.Messages
|
|||
ModeIndicators = message[5].Select(t => ParseModeIndicator(t)).ToArray();
|
||||
NumberOfSatellites = int.Parse(message[6], CultureInfo.InvariantCulture);
|
||||
Hdop = NmeaMessage.StringToDouble(message[7]);
|
||||
OrhometricHeight = NmeaMessage.StringToDouble(message[8]);
|
||||
OrthometricHeight = NmeaMessage.StringToDouble(message[8]);
|
||||
GeoidalSeparation = NmeaMessage.StringToDouble(message[9]);
|
||||
var timeInSeconds = StringToDouble(message[10]);
|
||||
if (!double.IsNaN(timeInSeconds))
|
||||
TimeSinceLastDgpsUpdate = TimeSpan.FromSeconds(timeInSeconds);
|
||||
else
|
||||
TimeSinceLastDgpsUpdate = TimeSpan.MaxValue;
|
||||
TimeSinceLastDgpsUpdate = null;
|
||||
if (message[11].Length > 0)
|
||||
DgpsStationId = message[11];
|
||||
|
||||
|
|
@ -236,7 +236,7 @@ namespace NmeaParser.Messages
|
|||
/// <summary>
|
||||
/// Orthometric height in meters (MSL reference)
|
||||
/// </summary>
|
||||
public double OrhometricHeight { get; }
|
||||
public double OrthometricHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Geoidal separation in meters - the difference between the earth ellipsoid surface and mean-sea-level (geoid) surface defined by the reference datum used in the position solution<br/>
|
||||
|
|
@ -247,7 +247,7 @@ namespace NmeaParser.Messages
|
|||
/// <summary>
|
||||
/// Age of differential data - <see cref="TimeSpan.MaxValue"/> if talker ID is GN, additional GNS messages follow with GP and/or GL Age of differential data
|
||||
/// </summary>
|
||||
public TimeSpan TimeSinceLastDgpsUpdate { get; }
|
||||
public TimeSpan? TimeSinceLastDgpsUpdate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// eference station ID1, range 0000-4095 - Null if talker ID is GN, additional GNS messages follow with GP and/or GL Reference station ID
|
||||
|
|
@ -258,5 +258,7 @@ namespace NmeaParser.Messages
|
|||
/// Navigational status
|
||||
/// </summary>
|
||||
public NavigationalStatus Status { get; }
|
||||
|
||||
TimeSpan ITimestampedMessage.Timestamp => FixTime;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
134
src/NmeaParser/Nmea/Grs.cs
Normal file
134
src/NmeaParser/Nmea/Grs.cs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// *******************************************************************************
|
||||
// * 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.Globalization;
|
||||
|
||||
namespace NmeaParser.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// GNSS Range Residuals
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This sentence is used to support Receiver Autonomous Integrity Monitoring (RAIM). Range residuals can be
|
||||
/// computed in two ways for this process. The basic measurement integration cycle of most navigation filters
|
||||
/// generates a set of residuals and uses these to update the position state of the receiver.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// These residuals can be reported with GRS, but because of the fact that these were used to generate the navigation
|
||||
/// solution they should be recomputed using the new solution in order to reflect the residuals for the position solution in
|
||||
/// the GGA or GNS sentence.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The MODE field should indicate which computation method was used. An integrity process that uses these
|
||||
/// range residuals would also require GGA or GNS, GSA, and GSV sentences to be sent.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If only GPS, or GLONASS, or Galileo, or BDS, or QZSS, or NavIC (IRNSS)is used for the reported position
|
||||
/// solution, the talker ID is GP, GL, GA, GB, GQ, GI respectively and the range residuals pertain to the individual
|
||||
/// system.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If GPS, GLONASS, Galileo, BDS, QZSS, NavIC (IRNSS) are combined to obtain the position solution multiple
|
||||
/// GRS sentences are produced, one with the GPS satellites, another with the GLONASS satellites, etc. Each of these
|
||||
/// GRS sentences shall have talker ID “GN”, to indicate that the satellites are used in a combined solution. The GNSS
|
||||
/// System ID data field identifies the specific satellite system. It is important to distinguish the residuals from those that
|
||||
/// would be produced by a GPS-only, GLONASS-only, etc. position solution. In general, the residuals for a combined
|
||||
/// solution will be different from the residual for a GPS-only, GLONASS-only, etc. solution.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When multiple GRS sentences are necessary, use of the NMEA TAG Block structure (§ 7) and the TAG Block
|
||||
/// Sentence-grouping Parameter (§ 7.9.3) reliably links the related sentences together over any transport medium.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When GRS sentences are provided with related GSA and/or GSV sentences, use of the NMEA TAG Block structure
|
||||
/// (§ 7) and the TAG Block Sentence-grouping Parameter (§ 7.9.3) reliably links the related (different sentence
|
||||
/// formatters) sentences together over any transport medium.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dtm")]
|
||||
[NmeaMessageType("--GRS")]
|
||||
public class Grs : NmeaMessage, ITimestampedMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines the way the <see cref="Grs"/> residuals were calculated.
|
||||
/// </summary>
|
||||
public enum GrsMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Residuals were used to calculate the position given in the matching GGA or GNS sentence
|
||||
/// </summary>
|
||||
UsedForPosition,
|
||||
/// <summary>
|
||||
/// Residuals were recomputed after the GGA or GNS position was computed
|
||||
/// </summary>
|
||||
RecomputedFromPosition
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Gbs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The message type</param>
|
||||
/// <param name="message">The NMEA message values.</param>
|
||||
public Grs (string type, string[] message) : base(type, message)
|
||||
{
|
||||
if (message == null || message.Length < 8)
|
||||
throw new ArgumentException("Invalid Grs", "message");
|
||||
FixTime = StringToTimeSpan(message[0]);
|
||||
Mode = message[1] == "1" ? GrsMode.RecomputedFromPosition : GrsMode.UsedForPosition;
|
||||
double[] residuals = new double[message.Length - 2];
|
||||
for (int i = 2; i < message.Length; i++)
|
||||
{
|
||||
residuals[i-2] = NmeaMessage.StringToDouble(message[i]);
|
||||
}
|
||||
Residuals = residuals;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UTC time of the GGA or GNS fix associated with this sentence
|
||||
/// </summary>
|
||||
public TimeSpan FixTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Residual calculation mode
|
||||
/// </summary>
|
||||
public GrsMode Mode { get; }
|
||||
|
||||
TimeSpan ITimestampedMessage.Timestamp => FixTime;
|
||||
|
||||
/// <summary>
|
||||
/// Range residuals in meters for satellites used in the navigation solution
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Order must match order of the satellite ID3 numbers in GSA. When GRS is used GSA and GSV are generally required
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Notes:
|
||||
/// <ul>
|
||||
/// <li>If the range residual exceeds +99.9 meters, then the decimal part is dropped, resulting in an integer (-103.7 becomes -103).
|
||||
/// The maximum value for this field is +999.</li>
|
||||
/// <li>The sense or sign of the range residual is determined by the order of parameters used in the calculation. The
|
||||
/// expected order is as follows: range residual = calculated range - measured range.</li>
|
||||
/// <li>When multiple GRS sentences are being sent then their order of transmission must match the order of
|
||||
/// corresponding GSA sentences.Listeners shall keep track of pairs of GSA and GRS sentences and discard data
|
||||
/// if pairs are incomplete.</li>
|
||||
/// </ul>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public double[] Residuals { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ namespace NmeaParser.Messages
|
|||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gpgst")]
|
||||
[NmeaMessageType("--GST")]
|
||||
public class Gst : NmeaMessage
|
||||
public class Gst : NmeaMessage, ITimestampedMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Gst"/> class.
|
||||
|
|
@ -82,5 +82,7 @@ namespace NmeaParser.Messages
|
|||
/// Standard deviation of altitude error in meters.
|
||||
/// </summary>
|
||||
public double SigmaHeightError { get; }
|
||||
|
||||
TimeSpan ITimestampedMessage.Timestamp => FixTime;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,19 @@ namespace NmeaParser.Messages
|
|||
return SatelliteSystem.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
switch (TalkerId)
|
||||
{
|
||||
case Talker.GlobalPositioningSystem: return $"GPS{Id}";
|
||||
case Talker.GlonassReceiver: return $"GLO{Id}";
|
||||
case Talker.GalileoPositioningSystem: return $"GAL{Id}";
|
||||
case Talker.BeiDouNavigationSatelliteSystem: return $"BEI{Id}";
|
||||
default: return Id.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
22
src/NmeaParser/Nmea/IGeographicLocation.cs
Normal file
22
src/NmeaParser/Nmea/IGeographicLocation.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NmeaParser.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates a message that contains a latitude and longitude value
|
||||
/// </summary>
|
||||
public interface IGeographicLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the latitude component of the location
|
||||
/// </summary>
|
||||
double Latitude { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the longitude component of the location
|
||||
/// </summary>
|
||||
double Longitude { get; }
|
||||
}
|
||||
}
|
||||
17
src/NmeaParser/Nmea/ITimestampedMessage.cs
Normal file
17
src/NmeaParser/Nmea/ITimestampedMessage.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NmeaParser.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates this message has a time stamp
|
||||
/// </summary>
|
||||
public interface ITimestampedMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the time of day for the message
|
||||
/// </summary>
|
||||
TimeSpan Timestamp { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -65,6 +65,7 @@ namespace NmeaParser.Messages
|
|||
{
|
||||
MessageType = messageType;
|
||||
MessageParts = messageParts;
|
||||
Timestamp = System.Diagnostics.Stopwatch.GetTimestamp() * 1000d / System.Diagnostics.Stopwatch.Frequency;
|
||||
}
|
||||
|
||||
static NmeaMessage()
|
||||
|
|
@ -137,38 +138,58 @@ namespace NmeaParser.Messages
|
|||
/// </summary>
|
||||
/// <param name="message">The NMEA message string.</param>
|
||||
/// <param name="previousSentence">The previously received message (only used if parsing multi-sentence messages)</param>
|
||||
/// <param name="ignoreChecksum">If <c>true</c> ignores the checksum completely, if <c>false</c> validates the checksum if present.</param>
|
||||
/// <returns>The nmea message that was parsed.</returns>
|
||||
/// <exception cref="System.ArgumentException">
|
||||
/// Invalid nmea message: Missing starting character '$'
|
||||
/// or checksum failure
|
||||
/// </exception>
|
||||
public static NmeaMessage Parse(string message, IMultiSentenceMessage? previousSentence = null)
|
||||
public static NmeaMessage Parse(string message, IMultiSentenceMessage? previousSentence = null, bool ignoreChecksum = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message))
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
|
||||
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)
|
||||
{
|
||||
checksum = Convert.ToInt32(message.Substring(idx + 1), 16);
|
||||
if (message.Length > idx + 1)
|
||||
{
|
||||
if (int.TryParse(message.Substring(idx + 1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int c))
|
||||
checksum = c;
|
||||
else
|
||||
throw new ArgumentException("Invalid checksum string");
|
||||
}
|
||||
message = message.Substring(0, message.IndexOf('*'));
|
||||
}
|
||||
if (checksum > -1)
|
||||
if (!ignoreChecksum && checksum > -1)
|
||||
{
|
||||
int checksumTest = 0;
|
||||
for (int i = 1; i < message.Length; i++)
|
||||
{
|
||||
checksumTest ^= Convert.ToByte(message[i]);
|
||||
var c = message[i];
|
||||
if (c < 0x20 || c > 0x7E)
|
||||
throw new System.IO.InvalidDataException("NMEA Message contains invalid characters");
|
||||
checksumTest ^= Convert.ToByte(c);
|
||||
}
|
||||
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));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 1; i < message.Length; i++)
|
||||
{
|
||||
if (message[i] < 0x20 || message[i] > 0x7E)
|
||||
throw new System.IO.InvalidDataException("NMEA Message contains invalid characters");
|
||||
}
|
||||
}
|
||||
|
||||
string[] parts = message.Split(new char[] { ',' });
|
||||
string MessageType = parts[0].Substring(1);
|
||||
if (MessageType == string.Empty)
|
||||
throw new ArgumentException("Missing NMEA Message Type");
|
||||
string[] MessageParts = parts.Skip(1).ToArray();
|
||||
if(previousSentence is NmeaMessage pmsg && pmsg.MessageType.Substring(2) == MessageType.Substring(2))
|
||||
{
|
||||
|
|
@ -233,7 +254,9 @@ namespace NmeaParser.Messages
|
|||
checksumTest ^= 0x2C; //Comma separator
|
||||
for (int i = 0; i < message.Length; i++)
|
||||
{
|
||||
checksumTest ^= Convert.ToByte(message[i]);
|
||||
var c = message[i];
|
||||
if (c < 256)
|
||||
checksumTest ^= Convert.ToByte(c);
|
||||
}
|
||||
}
|
||||
return Convert.ToByte(checksumTest);
|
||||
|
|
@ -277,5 +300,14 @@ namespace NmeaParser.Messages
|
|||
}
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a relative timestamp in milliseconds indicating the time the message was created.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value is deduced from <c>System.Diagnostics.Stopwatch.GetTimestamp() * 1000d / System.Diagnostics.Stopwatch.Frequency</c>.
|
||||
/// You can use it to calculate the age of the message in milliseconds by calculating the difference between the timestamp and the above expression
|
||||
/// </remarks>
|
||||
public double Timestamp { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ namespace NmeaParser.Messages
|
|||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gprmb")]
|
||||
[NmeaMessageType("--RMA")]
|
||||
public class Rma : NmeaMessage
|
||||
public class Rma : NmeaMessage, IGeographicLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Positioning system status field
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ namespace NmeaParser.Messages
|
|||
/// Recommended minimum navigation information
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Navigation data from present position to a destination waypoint provided by a Loran-C, GNSS, DECCA, navigatin computer
|
||||
/// <para>Navigation data from present position to a destination waypoint provided by a Loran-C, GNSS, DECCA, navigation computer
|
||||
/// or other integrated navigation system.</para>
|
||||
/// <para>
|
||||
/// This sentence always accompanies <see cref="Rma"/> and <see cref="Rmc"/> sentences when a destination is active when provided by a Loran-C or GNSS receiver,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace NmeaParser.Messages
|
|||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Gprmc")]
|
||||
[NmeaMessageType("--RMC")]
|
||||
public class Rmc : NmeaMessage
|
||||
public class Rmc : NmeaMessage, ITimestampedMessage, IGeographicLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Rmc"/> class.
|
||||
|
|
@ -93,5 +93,7 @@ namespace NmeaParser.Messages
|
|||
/// Magnetic Variation
|
||||
/// </summary>
|
||||
public double MagneticVariation { get; }
|
||||
|
||||
TimeSpan ITimestampedMessage.Timestamp => FixTime.TimeOfDay;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
src/NmeaParser/Nmea/Vlw.cs
Normal file
56
src/NmeaParser/Nmea/Vlw.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// *******************************************************************************
|
||||
// * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// * you may not use this file except in compliance with the License.
|
||||
// * You may obtain a copy of the License at
|
||||
// *
|
||||
// * http://www.apache.org/licenses/LICENSE-2.0
|
||||
// *
|
||||
// * Unless required by applicable law or agreed to in writing, software
|
||||
// * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// * See the License for the specific language governing permissions and
|
||||
// * limitations under the License.
|
||||
// ******************************************************************************
|
||||
|
||||
using System;
|
||||
|
||||
namespace NmeaParser.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Dual Ground/Water Distance
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The distance traveled, relative to the water and over the ground.
|
||||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dtm")]
|
||||
[NmeaMessageType("--VLW")]
|
||||
public class Vlw : NmeaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Vlw"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The message type</param>
|
||||
/// <param name="message">The NMEA message values.</param>
|
||||
public Vlw (string type, string[] message) : base(type, message)
|
||||
{
|
||||
if (message == null || message.Length < 7)
|
||||
throw new ArgumentException("Invalid VLW", "message");
|
||||
WaterDistanceCumulative = NmeaMessage.StringToDouble(message[0]);
|
||||
WaterDistanceSinceReset = NmeaMessage.StringToDouble(message[2]);
|
||||
GroundDistanceCumulative = NmeaMessage.StringToDouble(message[4]);
|
||||
GroundDistanceSinceReset = NmeaMessage.StringToDouble(message[6]);
|
||||
}
|
||||
|
||||
/// <summary>Total cumulative water distance, nautical miles</summary>
|
||||
public double WaterDistanceCumulative { get; }
|
||||
|
||||
/// <summary>Water distance since reset, nautical miles</summary>
|
||||
public double WaterDistanceSinceReset { get; }
|
||||
|
||||
/// <summary>Total cumulative ground distance, nautical miles</summary>
|
||||
public double GroundDistanceCumulative { get; }
|
||||
|
||||
/// <summary>Ground distance since reset, nautical miles</summary>
|
||||
public double GroundDistanceSinceReset { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ namespace NmeaParser.Messages
|
|||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Zda")]
|
||||
[NmeaMessageType("--ZDA")]
|
||||
public class Zda : NmeaMessage
|
||||
public class Zda : NmeaMessage, ITimestampedMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Zda"/> class.
|
||||
|
|
@ -53,5 +53,7 @@ namespace NmeaParser.Messages
|
|||
/// Gets the time of fix
|
||||
/// </summary>
|
||||
public DateTimeOffset FixDateTime { get; }
|
||||
|
||||
TimeSpan ITimestampedMessage.Timestamp => FixDateTime.TimeOfDay;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,52 +11,52 @@
|
|||
// * 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using NmeaParser.Messages;
|
||||
|
||||
namespace NmeaParser
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic abstract NMEA device
|
||||
/// </summary>
|
||||
public abstract class NmeaDevice : IDisposable
|
||||
{
|
||||
private readonly object m_lockObject = new object();
|
||||
private string m_message = "";
|
||||
private Stream? m_stream;
|
||||
private CancellationTokenSource? m_cts;
|
||||
namespace NmeaParser
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic abstract NMEA device
|
||||
/// </summary>
|
||||
public abstract class NmeaDevice : IDisposable
|
||||
{
|
||||
private readonly object m_lockObject = new object();
|
||||
private string m_message = "";
|
||||
private Stream? m_stream;
|
||||
private CancellationTokenSource? m_cts;
|
||||
private bool m_isOpening;
|
||||
private Task? m_ParserTask;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NmeaDevice"/> class.
|
||||
/// </summary>
|
||||
protected NmeaDevice()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NmeaDevice"/> class.
|
||||
/// </summary>
|
||||
protected NmeaDevice()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and opens the stream the <see cref="NmeaDevice"/> will be working on top off.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous action.</returns>
|
||||
public async Task OpenAsync()
|
||||
{
|
||||
lock (m_lockObject)
|
||||
{
|
||||
if (IsOpen || m_isOpening) return;
|
||||
/// <returns>A task that represents the asynchronous action.</returns>
|
||||
public async Task OpenAsync()
|
||||
{
|
||||
lock (m_lockObject)
|
||||
{
|
||||
if (IsOpen || m_isOpening) return;
|
||||
m_isOpening = true;
|
||||
}
|
||||
m_cts = new CancellationTokenSource();
|
||||
m_stream = await OpenStreamAsync();
|
||||
StartParser(m_cts.Token);
|
||||
_lastMultiMessage = null;
|
||||
}
|
||||
m_cts = new CancellationTokenSource();
|
||||
m_stream = await OpenStreamAsync();
|
||||
StartParser(m_cts.Token);
|
||||
_lastMultiMessage = null;
|
||||
lock (m_lockObject)
|
||||
{
|
||||
IsOpen = true;
|
||||
|
|
@ -64,32 +64,32 @@ namespace NmeaParser
|
|||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "_")]
|
||||
private void StartParser(CancellationToken token)
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "_")]
|
||||
private void StartParser(CancellationToken token)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("Starting parser...");
|
||||
m_ParserTask = Task.Run(async () =>
|
||||
{
|
||||
byte[] buffer = new byte[1024];
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
int readCount = 0;
|
||||
try
|
||||
{
|
||||
readCount = await ReadAsync(buffer, 0, 1024, token).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
if (token.IsCancellationRequested)
|
||||
break;
|
||||
if (readCount > 0)
|
||||
{
|
||||
OnData(buffer.Take(readCount).ToArray());
|
||||
}
|
||||
await Task.Delay(50);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
m_ParserTask = Task.Run(async () =>
|
||||
{
|
||||
byte[] buffer = new byte[1024];
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
int readCount = 0;
|
||||
try
|
||||
{
|
||||
readCount = await ReadAsync(buffer, 0, 1024, token).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
if (token.IsCancellationRequested)
|
||||
break;
|
||||
if (readCount > 0)
|
||||
{
|
||||
OnData(buffer, readCount);
|
||||
}
|
||||
await Task.Yield();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a read operation of the stream
|
||||
/// </summary>
|
||||
|
|
@ -97,13 +97,13 @@ namespace NmeaParser
|
|||
/// <param name="offset">The byte offset in buffer at which to begin writing data from the stream.</param>
|
||||
/// <param name="count">The maximum number of bytes to read.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is System.Threading.CancellationToken.None.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous read operation. The value of the TResult
|
||||
/// parameter contains the total number of bytes read into the buffer. The result
|
||||
/// value can be less than the number of bytes requested if the number of bytes currently
|
||||
/// available is less than the requested number, or it can be 0 (zero) if the end
|
||||
/// of the stream has been reached.
|
||||
/// </returns>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous read operation. The value of the TResult
|
||||
/// parameter contains the total number of bytes read into the buffer. The result
|
||||
/// value can be less than the number of bytes requested if the number of bytes currently
|
||||
/// available is less than the requested number, or it can be 0 (zero) if the end
|
||||
/// of the stream has been reached.
|
||||
/// </returns>
|
||||
protected virtual Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (m_stream == null)
|
||||
|
|
@ -111,180 +111,182 @@ namespace NmeaParser
|
|||
return m_stream.ReadAsync(buffer, 0, 1024, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and opens the stream the NmeaDevice is working on top off.
|
||||
/// </summary>
|
||||
/// <returns>The opened data stream.</returns>
|
||||
/// <seealso cref="CloseStreamAsync(Stream)"/>
|
||||
protected abstract Task<Stream> OpenStreamAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Closes the device.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous action.</returns>
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
if (m_cts != null)
|
||||
{
|
||||
if (m_cts != null)
|
||||
m_cts.Cancel();
|
||||
m_cts = null;
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates and opens the stream the NmeaDevice is working on top off.
|
||||
/// </summary>
|
||||
/// <returns>The opened data stream.</returns>
|
||||
/// <seealso cref="CloseStreamAsync(Stream)"/>
|
||||
protected abstract Task<Stream> OpenStreamAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Closes the device.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous action.</returns>
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
if (m_cts != null)
|
||||
{
|
||||
if (m_cts != null)
|
||||
m_cts.Cancel();
|
||||
m_cts = null;
|
||||
}
|
||||
if (m_ParserTask != null)
|
||||
await m_ParserTask;
|
||||
await m_ParserTask;
|
||||
if (m_stream != null)
|
||||
await CloseStreamAsync(m_stream);
|
||||
_lastMultiMessage = null;
|
||||
await CloseStreamAsync(m_stream);
|
||||
_lastMultiMessage = null;
|
||||
m_stream = null;
|
||||
lock (m_lockObject)
|
||||
{
|
||||
m_isOpening = false;
|
||||
IsOpen = false;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Closes the stream the NmeaDevice is working on top off.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to be closed.</param>
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Closes the stream the NmeaDevice is working on top off.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to be closed.</param>
|
||||
/// <returns>A task that represents the asynchronous action.</returns>
|
||||
/// <seealso cref="OpenStreamAsync"/>
|
||||
protected abstract Task CloseStreamAsync(Stream stream);
|
||||
|
||||
private void OnData(byte[] data)
|
||||
{
|
||||
var nmea = System.Text.Encoding.UTF8.GetString(data, 0, data.Length);
|
||||
List<string> lines = new List<string>();
|
||||
lock (m_lockObject)
|
||||
{
|
||||
m_message += nmea;
|
||||
|
||||
var lineEnd = m_message.IndexOf("\n", StringComparison.Ordinal);
|
||||
while (lineEnd > -1)
|
||||
{
|
||||
string line = m_message.Substring(0, lineEnd).Trim();
|
||||
m_message = m_message.Substring(lineEnd + 1);
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
lines.Add(line);
|
||||
lineEnd = m_message.IndexOf("\n", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
foreach(var line in lines)
|
||||
ProcessMessage(line);
|
||||
protected abstract Task CloseStreamAsync(Stream stream);
|
||||
|
||||
private void OnData(byte[] data, int count)
|
||||
{
|
||||
var nmea = System.Text.Encoding.UTF8.GetString(data, 0, count);
|
||||
List<string> lines = new List<string>();
|
||||
lock (m_lockObject)
|
||||
{
|
||||
m_message += nmea;
|
||||
|
||||
var lineEnd = m_message.IndexOf("\n", StringComparison.Ordinal);
|
||||
while (lineEnd > -1)
|
||||
{
|
||||
string line = m_message.Substring(0, lineEnd).Trim();
|
||||
m_message = m_message.Substring(lineEnd + 1);
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
lines.Add(line);
|
||||
lineEnd = m_message.IndexOf("\n", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
foreach(var line in lines)
|
||||
ProcessMessage(line);
|
||||
}
|
||||
|
||||
private IMultiSentenceMessage? _lastMultiMessage;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification="Must silently handle invalid/corrupt input")]
|
||||
private void ProcessMessage(string p)
|
||||
{
|
||||
try
|
||||
{
|
||||
var msg = NmeaMessage.Parse(p, _lastMultiMessage);
|
||||
private IMultiSentenceMessage? _lastMultiMessage;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification="Must silently handle invalid/corrupt input")]
|
||||
private void ProcessMessage(string p)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (p.Length == 0 || p[0] != '$')
|
||||
return;
|
||||
var msg = NmeaMessage.Parse(p, _lastMultiMessage);
|
||||
if(msg is IMultiSentenceMessage multi)
|
||||
{
|
||||
if (!multi.IsComplete)
|
||||
{
|
||||
_lastMultiMessage = multi; //Keep it around until next time
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_lastMultiMessage = null;
|
||||
if (msg != null)
|
||||
OnMessageReceived(msg);
|
||||
}
|
||||
catch { }
|
||||
if (msg != null)
|
||||
OnMessageReceived(msg);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void OnMessageReceived(NmeaMessage msg)
|
||||
{
|
||||
if (msg == null)
|
||||
return;
|
||||
{
|
||||
if (msg == null)
|
||||
return;
|
||||
|
||||
MessageReceived?.Invoke(this, new NmeaMessageReceivedEventArgs(msg));
|
||||
}
|
||||
|
||||
//private readonly Dictionary<string, Dictionary<int, Nmea.NmeaMessage>> MultiPartMessageCache = new Dictionary<string,Dictionary<int,Nmea.NmeaMessage>>();
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an NMEA message is received.
|
||||
/// </summary>
|
||||
public event EventHandler<NmeaMessageReceivedEventArgs>? MessageReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (m_stream != null)
|
||||
{
|
||||
if (m_cts != null)
|
||||
{
|
||||
m_cts.Cancel();
|
||||
m_cts = null;
|
||||
}
|
||||
CloseStreamAsync(m_stream);
|
||||
if (disposing && m_stream != null)
|
||||
m_stream.Dispose();
|
||||
m_stream = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this device is open.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is open; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsOpen { get; private set; }
|
||||
|
||||
}
|
||||
|
||||
//private readonly Dictionary<string, Dictionary<int, Nmea.NmeaMessage>> MultiPartMessageCache = new Dictionary<string,Dictionary<int,Nmea.NmeaMessage>>();
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an NMEA message is received.
|
||||
/// </summary>
|
||||
public event EventHandler<NmeaMessageReceivedEventArgs>? MessageReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (m_stream != null)
|
||||
{
|
||||
if (m_cts != null)
|
||||
{
|
||||
m_cts.Cancel();
|
||||
m_cts = null;
|
||||
}
|
||||
CloseStreamAsync(m_stream);
|
||||
if (disposing && m_stream != null)
|
||||
m_stream.Dispose();
|
||||
m_stream = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this device is open.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is open; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsOpen { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this device supports writing
|
||||
/// </summary>
|
||||
/// <seealso cref="WriteAsync(byte[], int, int)"/>
|
||||
public virtual bool CanWrite { get => false; }
|
||||
|
||||
/// </summary>
|
||||
/// <seealso cref="WriteAsync(byte[], int, int)"/>
|
||||
public virtual bool CanWrite { get => false; }
|
||||
|
||||
/// <summary>
|
||||
/// Writes to the device stream. Useful for transmitting RTCM corrections to the device
|
||||
/// Check the <see cref="CanWrite"/> property before calling this method.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The byte array that contains the data to write to the port.</param>
|
||||
/// <param name="offset">The zero-based byte offset in the buffer parameter at which to begin copying
|
||||
/// bytes to the port.</param>
|
||||
/// <param name="length">The number of bytes to write.</param>
|
||||
/// <returns>Task</returns>
|
||||
/// <seealso cref="CanWrite"/>
|
||||
/// <param name="buffer">The byte array that contains the data to write to the port.</param>
|
||||
/// <param name="offset">The zero-based byte offset in the buffer parameter at which to begin copying
|
||||
/// bytes to the port.</param>
|
||||
/// <param name="length">The number of bytes to write.</param>
|
||||
/// <returns>Task</returns>
|
||||
/// <seealso cref="CanWrite"/>
|
||||
public virtual Task WriteAsync(byte[] buffer, int offset, int length)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event argument for the <see cref="NmeaDevice.MessageReceived" />
|
||||
/// </summary>
|
||||
public sealed class NmeaMessageReceivedEventArgs : EventArgs
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event argument for the <see cref="NmeaDevice.MessageReceived" />
|
||||
/// </summary>
|
||||
public sealed class NmeaMessageReceivedEventArgs : EventArgs
|
||||
{
|
||||
internal NmeaMessageReceivedEventArgs(NmeaMessage message)
|
||||
{
|
||||
Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the nmea message.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The nmea message.
|
||||
/// </value>
|
||||
{
|
||||
Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the nmea message.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The nmea message.
|
||||
/// </value>
|
||||
public NmeaMessage Message { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="MSBuild.Sdk.Extras/2.0.54">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard1.4;netcoreapp2.1;net451;monoandroid50;monoandroid70;xamarinios10;uap10.0.16299</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;netstandard1.4;netcoreapp2.1;net451;monoandroid50;monoandroid70;xamarinios10;uap10.0.16299</TargetFrameworks>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
|
|
@ -9,9 +9,9 @@
|
|||
<Authors>Morten Nielsen</Authors>
|
||||
<Company>Morten Nielsen</Company>
|
||||
<Description>An NMEA stream parser for serial port, bluetooth and file-based nmea simulation.</Description>
|
||||
<PackageTags>nmea winrt wpf uwp xamarin gps serialport bluetooth</PackageTags>
|
||||
<PackageTags>NMEA GPS GNSS Serialport Bluetooth Navigation NTRIP RTCM Galileo GLONASS BeiDou Garmin Trimble</PackageTags>
|
||||
<PackageId>SharpGIS.NmeaParser</PackageId>
|
||||
<Version>2.0</Version>
|
||||
<Version>2.1</Version>
|
||||
<Product>NMEA Parser</Product>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<PackageProjectUrl>https://dotmorten.github.io/NmeaParser/</PackageProjectUrl>
|
||||
|
|
@ -20,7 +20,12 @@
|
|||
<Copyright>Copyright © Morten Nielsen 2015-2020</Copyright>
|
||||
<OutputPath>$(MSBuildThisFileDirectory)..\..\artifacts\NmeaParser\$(Configuration)</OutputPath>
|
||||
<PackageOutputPath>..\..\artifacts\NuGet\$(Configuration)\</PackageOutputPath>
|
||||
<PackageReleaseNotes>New refined and easier to use v2 API</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>Added ITimestampedMessage interface to easily get time stamps from any message that supports it.
|
||||
Added IGeographicLocation interface to easily get any message that supports reporting a location.
|
||||
Added GnssMonitor for simplifying monitoring location messages and reporting a location.
|
||||
Added an NTRIP client for getting RTCM correctional messages from an NTRIP Server.
|
||||
Improved the buffered stream to more accurately emulate baud rates.
|
||||
All messages now have a unique timestamp useful to expire older messages.</PackageReleaseNotes>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
|
|
@ -30,11 +35,15 @@
|
|||
<Nullable>enable</Nullable>
|
||||
<PackageIcon>logo.png</PackageIcon>
|
||||
<PackageIconUrl />
|
||||
<AssemblyVersion>2.1.0.0</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard1.4'">
|
||||
<DefineConstants>$(DefineConstants);NETSTANDARD</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<DefineConstants>$(DefineConstants);NETSTANDARD</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'net451'">
|
||||
<DefineConstants>$(DefineConstants);NETFX</DefineConstants>
|
||||
|
|
@ -66,7 +75,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19270-01" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
|
|
@ -15,7 +14,7 @@ namespace SampleApp.Droid
|
|||
{
|
||||
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
|
||||
public partial class Resource
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<PackageCertificateKeyFile>SampleApp.UWP_TemporaryKey.pfx</PackageCertificateKeyFile>
|
||||
<PackageCertificateThumbprint>D89DCA9AB58E8FD65E1F0E01AF1A7AFC277C1706</PackageCertificateThumbprint>
|
||||
<PackageCertificateThumbprint>F2A07DA2EDDDF050675AAFAB0507AA6E66A355F0</PackageCertificateThumbprint>
|
||||
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -32,7 +32,7 @@
|
|||
</StackPanel>
|
||||
</local:KeyValuePairControl.Value>
|
||||
</local:KeyValuePairControl>
|
||||
<local:KeyValuePairControl Header="Last DGPS update: " Value="{Binding TimeSinceLastDgpsUpdate, Mode=OneTime}" />
|
||||
<local:KeyValuePairControl Header="Last DGPS update: " Value="{Binding TimeSinceLastDgpsUpdate, Mode=OneTime, FallbackValue=Never}" />
|
||||
<local:KeyValuePairControl Header="DGPS ID: " Value="{Binding DgpsStationId, Mode=OneTime}" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
|
|
|||
28
src/SampleApp.WinDesktop/GnssMonitorView.xaml
Normal file
28
src/SampleApp.WinDesktop/GnssMonitorView.xaml
Normal 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="CornflowerBlue" Padding="20,10" Margin="-10,-10,-10,5" x:Name="HeaderPanel">
|
||||
<TextBlock Text="GNSS Monitor" FontSize="20" FontWeight="Bold" Foreground="White" />
|
||||
</Border>
|
||||
<ItemsControl x:Name="Values" Margin="20,10">
|
||||
<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" FontWeight="Bold" />
|
||||
<TextBlock Text="{Binding Value}" TextWrapping="Wrap" Grid.Column="1" VerticalAlignment="Top" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
76
src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs
Normal file
76
src/SampleApp.WinDesktop/GnssMonitorView.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
</Border>
|
||||
<local:KeyValuePairControl Header="GPS Mode: " Value="{Binding Mode}"/>
|
||||
<local:KeyValuePairControl Header="Fix Mode: " Value="{Binding Fix, Mode=OneTime}" />
|
||||
<local:KeyValuePairControl Header="Satellite vehicles: " Value="{Binding SatelliteIDs, Mode=OneTime}" />
|
||||
<local:KeyValuePairControl Header="Satellite vehicles: " x:Name="vehicles" />
|
||||
<local:KeyValuePairControl Header="HDOP: " Value="{Binding Hdop, Mode=OneTime}" />
|
||||
<local:KeyValuePairControl Header="VDOP: " Value="{Binding Vdop, Mode=OneTime}" />
|
||||
<local:KeyValuePairControl Header="PDOP: " Value="{Binding Pdop, Mode=OneTime}" />
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ namespace SampleApp.WinDesktop
|
|||
}
|
||||
|
||||
public static readonly DependencyProperty MessageProperty =
|
||||
DependencyProperty.Register(nameof(Message), typeof(Gsa), typeof(GsaControl), new PropertyMetadata(null));
|
||||
}
|
||||
DependencyProperty.Register(nameof(Message), typeof(Gsa), typeof(GsaControl), new PropertyMetadata(null, (d, e) => ((GsaControl)d).OnGsaPropertyChanged(e)));
|
||||
|
||||
private void OnGsaPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (Message == null)
|
||||
vehicles.Value = null;
|
||||
else
|
||||
vehicles.Value = string.Join(",", Message?.SatelliteIDs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:SampleApp.WinDesktop"
|
||||
xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
|
||||
Title="Sample App" Height="500" Width="625">
|
||||
Title="NMEA Parser Sample App" Height="768" Width="1024">
|
||||
<Window.Resources>
|
||||
<local:NullToCollapsedConverter x:Key="nullConv" />
|
||||
<Style TargetType="UserControl" x:Key="card">
|
||||
|
|
@ -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}}" />
|
||||
|
|
@ -35,21 +35,26 @@
|
|||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="100" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<local:SatelliteView MaxWidth="{Binding ActualHeight, ElementName=satView}"
|
||||
Grid.Column="1" x:Name="satView" />
|
||||
<local:SatelliteSnr Grid.Row="1"
|
||||
GsvMessage="{Binding GsvMessage, ElementName=satView}" />
|
||||
<local:SatelliteSnr Grid.Row="1" x:Name="satSnr" />
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="GNSS Monitor">
|
||||
<local:GnssMonitorView x:Name="gnssMonitorView" />
|
||||
</TabItem>
|
||||
<TabItem Header="Deviation Map">
|
||||
<local:PointPlotView x:Name="mapplot" />
|
||||
</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"
|
||||
|
|
@ -65,18 +70,12 @@
|
|||
<TextBlock Text="None" x:Name="currentDeviceInfo" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
<Button Width="200" Content="Open NMEA Log..." Click="OpenNmeaLogButton_Click" HorizontalAlignment="Left" Padding="20,5" Margin="0,5" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Width="200" Content="Auto-discover serial port"
|
||||
HorizontalAlignment="Left" Padding="20,5" Margin="0,5"
|
||||
Click="AutoDiscoverButton_Click" />
|
||||
<TextBlock x:Name="autoDiscoverStatus" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<GroupBox Header="Open Serial device" Width="200" HorizontalAlignment="Left">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Serial port:" />
|
||||
<ComboBox x:Name="serialPorts" />
|
||||
<TextBlock Text="Baud rate:" />
|
||||
<ComboBox x:Name="baudRates" SelectedIndex="3">
|
||||
<ComboBox x:Name="baudRates" SelectedIndex="7">
|
||||
<ComboBoxItem>1200</ComboBoxItem>
|
||||
<ComboBoxItem>2400</ComboBoxItem>
|
||||
<ComboBoxItem>4800</ComboBoxItem>
|
||||
|
|
@ -92,6 +91,9 @@
|
|||
</StackPanel>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="NTRIP">
|
||||
<local:NtripView />
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using NmeaParser.Gnss;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -14,7 +15,8 @@ namespace SampleApp.WinDesktop
|
|||
public partial class MainWindow : Window
|
||||
{
|
||||
private Queue<string> messages = new Queue<string>(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()
|
||||
{
|
||||
|
|
@ -36,15 +38,15 @@ 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");
|
||||
StartDevice(device);
|
||||
var device = new NmeaParser.NmeaFileDevice("NmeaSampleData.txt") { EmulatedBaudRate = 9600, BurstRate = TimeSpan.FromSeconds(1d) };
|
||||
_ = StartDevice(device);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloads the current device, and opens the next device
|
||||
/// </summary>
|
||||
/// <param name="device"></param>
|
||||
private async void StartDevice(NmeaParser.NmeaDevice device)
|
||||
private async Task StartDevice(NmeaParser.NmeaDevice device)
|
||||
{
|
||||
//Clean up old device
|
||||
if (currentDevice != null)
|
||||
|
|
@ -53,6 +55,12 @@ namespace SampleApp.WinDesktop
|
|||
if (currentDevice.IsOpen)
|
||||
await currentDevice.CloseAsync();
|
||||
currentDevice.Dispose();
|
||||
if (gnssMonitorView.Monitor != null)
|
||||
{
|
||||
gnssMonitorView.Monitor.LocationChanged -= Monitor_LocationChanged;
|
||||
gnssMonitorView.Monitor = null;
|
||||
}
|
||||
mapplot.Clear();
|
||||
}
|
||||
output.Text = "";
|
||||
messages.Clear();
|
||||
|
|
@ -61,9 +69,14 @@ namespace SampleApp.WinDesktop
|
|||
gpgsaView.Message = null;
|
||||
gpgllView.Message = null;
|
||||
pgrmeView.Message = null;
|
||||
satView.GsvMessage = null;
|
||||
satView.ClearGsv();
|
||||
satSnr.ClearGsv();
|
||||
//Start new device
|
||||
currentDevice = device;
|
||||
foreach(var child in MessagePanel.Children.OfType<UnknownMessageControl>().ToArray())
|
||||
{
|
||||
MessagePanel.Children.Remove(child);
|
||||
}
|
||||
currentDevice.MessageReceived += device_MessageReceived;
|
||||
view2d.NmeaDevice = device;
|
||||
view3d.NmeaDevice = device;
|
||||
|
|
@ -76,6 +89,15 @@ namespace SampleApp.WinDesktop
|
|||
((NmeaParser.SerialPortDevice)device).Port.PortName,
|
||||
((NmeaParser.SerialPortDevice)device).Port.BaudRate);
|
||||
}
|
||||
await device.OpenAsync();
|
||||
gnssMonitorView.Monitor = new GnssMonitor(device);
|
||||
gnssMonitorView.Monitor.LocationChanged += Monitor_LocationChanged;
|
||||
}
|
||||
|
||||
private void Monitor_LocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
var mon = sender as GnssMonitor;
|
||||
mapplot.AddLocation(mon.Latitude, mon.Longitude, mon.Altitude, mon.FixQuality);
|
||||
}
|
||||
|
||||
private void device_MessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs args)
|
||||
|
|
@ -89,7 +111,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;
|
||||
|
|
@ -118,108 +141,46 @@ 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)
|
||||
{
|
||||
MessageBox.Show("Error connecting: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
//Attempts to perform an auto discovery of serial ports
|
||||
private async void AutoDiscoverButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var button = sender as Button;
|
||||
button.IsEnabled = false;
|
||||
System.IO.Ports.SerialPort port = await Task.Run<System.IO.Ports.SerialPort>(() => {
|
||||
return FindPort(
|
||||
new System.Progress<string>((s) => { Dispatcher.BeginInvoke((Action)delegate() { autoDiscoverStatus.Text = s; }); }));
|
||||
});
|
||||
if (port != null) //we found a port
|
||||
{
|
||||
autoDiscoverStatus.Text = "";
|
||||
serialPorts.Text = port.PortName;
|
||||
baudRates.Text = port.BaudRate.ToString();
|
||||
ConnectToSerialButton_Click(sender, e);
|
||||
}
|
||||
else
|
||||
autoDiscoverStatus.Text = "No GPS port found";
|
||||
button.IsEnabled = false;
|
||||
}
|
||||
|
||||
//Iterates all serial ports and attempts to open them at different baud rates
|
||||
//and looks for a GPS message.
|
||||
private static System.IO.Ports.SerialPort FindPort(IProgress<string> progress = null)
|
||||
{
|
||||
var ports = System.IO.Ports.SerialPort.GetPortNames().OrderBy(s => s);
|
||||
foreach (var portName in ports)
|
||||
{
|
||||
using (var port = new System.IO.Ports.SerialPort(portName))
|
||||
{
|
||||
var defaultRate = port.BaudRate;
|
||||
List<int> baudRatesToTest = new List<int>(new[] { 9600, 4800, 115200, 19200, 57600, 38400, 2400 }); //Ordered by likelihood
|
||||
//Move default rate to first spot
|
||||
if (baudRatesToTest.Contains(defaultRate)) baudRatesToTest.Remove(defaultRate);
|
||||
baudRatesToTest.Insert(0, defaultRate);
|
||||
foreach (var baud in baudRatesToTest)
|
||||
{
|
||||
|
||||
if (progress != null)
|
||||
progress.Report(string.Format("Trying {0} @ {1}baud", portName, port.BaudRate));
|
||||
port.BaudRate = baud;
|
||||
port.ReadTimeout = 2000; //this might not be long enough
|
||||
bool success = false;
|
||||
try
|
||||
{
|
||||
port.Open();
|
||||
if (!port.IsOpen)
|
||||
continue; //couldn't open port
|
||||
try
|
||||
{
|
||||
port.ReadTo("$GP");
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
//Error reading
|
||||
}
|
||||
finally
|
||||
{
|
||||
port.Close();
|
||||
}
|
||||
if (success)
|
||||
{
|
||||
return new System.IO.Ports.SerialPort(portName, baud);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class ReverseConverter : IValueConverter
|
||||
|
|
|
|||
107
src/SampleApp.WinDesktop/NmeaLocationDataSource.cs
Normal file
107
src/SampleApp.WinDesktop/NmeaLocationDataSource.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// *******************************************************************************
|
||||
// * 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 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
|
||||
|
||||
/// <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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
protected async override Task OnStartAsync()
|
||||
{
|
||||
m_gnssMonitor.LocationChanged += OnLocationChanged;
|
||||
m_gnssMonitor.LocationLost += OnLocationChanged;
|
||||
if (m_startStopDevice && !this.m_gnssMonitor.Device.IsOpen)
|
||||
await this.m_gnssMonitor.Device.OpenAsync();
|
||||
|
||||
if (m_gnssMonitor.IsFixValid)
|
||||
OnLocationChanged(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected override Task OnStopAsync()
|
||||
{
|
||||
m_gnssMonitor.LocationChanged -= OnLocationChanged;
|
||||
m_gnssMonitor.LocationLost -= OnLocationChanged;
|
||||
if(m_startStopDevice)
|
||||
return m_gnssMonitor.Device.CloseAsync();
|
||||
else
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Esri.ArcGISRuntime.Location.Location currentLocation;
|
||||
|
||||
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;
|
||||
DateTimeOffset? timestamp = null;
|
||||
if(m_gnssMonitor.FixTime.HasValue)
|
||||
timestamp = new DateTimeOffset(DateTime.UtcNow.Date.Add(m_gnssMonitor.FixTime.Value));
|
||||
var location = new Esri.ArcGISRuntime.Location.Location(
|
||||
timestamp: timestamp,
|
||||
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);
|
||||
// Avoid raising additional location events if nothing changed
|
||||
if (currentLocation == null ||
|
||||
currentLocation.Position.X != location.Position.X ||
|
||||
currentLocation.Position.Y != location.Position.Y ||
|
||||
currentLocation.Position.Z != location.Position.Z ||
|
||||
currentLocation.Course != location.Course ||
|
||||
currentLocation.Velocity != location.Velocity ||
|
||||
currentLocation.HorizontalAccuracy != location.HorizontalAccuracy ||
|
||||
currentLocation.VerticalAccuracy != location.VerticalAccuracy ||
|
||||
currentLocation.IsLastKnown != location.IsLastKnown ||
|
||||
timestamp != location.Timestamp)
|
||||
{
|
||||
currentLocation = location;
|
||||
UpdateLocation(currentLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<bool>.FromResult(true);
|
||||
}
|
||||
|
||||
protected override Task OnStopAsync()
|
||||
{
|
||||
m_Accuracy = double.NaN;
|
||||
if(this.device != null)
|
||||
return this.device.CloseAsync();
|
||||
else
|
||||
return System.Threading.Tasks.Task<bool>.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; }
|
||||
}
|
||||
}
|
||||
74
src/SampleApp.WinDesktop/NtripView.xaml
Normal file
74
src/SampleApp.WinDesktop/NtripView.xaml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<UserControl x:Class="SampleApp.WinDesktop.NtripView"
|
||||
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="450" d:DesignWidth="800">
|
||||
<Grid >
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" MinWidth="80" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="Server: " />
|
||||
<TextBox Text="esricaster.esri.com" Grid.Column="1" x:Name="host" />
|
||||
<TextBlock Text="Port: " Grid.Row="1" />
|
||||
<TextBox Text="2101" Grid.Row="1" Grid.Column="1" x:Name="port" />
|
||||
<TextBlock Text="Username: " Grid.Row="2" />
|
||||
<TextBox Text="" Grid.Row="2" Grid.Column="1" x:Name="username" />
|
||||
<TextBlock Text="Password: " Grid.Row="3" />
|
||||
<PasswordBox Grid.Row="3" Grid.Column="1" x:Name="password" />
|
||||
<Button Content="Get Available Streams" Grid.Row="4" Grid.ColumnSpan="2" HorizontalAlignment="Left" Margin="5" Click="Button_Click"/>
|
||||
<TextBlock Text="Available streams:" Grid.Row="5" Grid.ColumnSpan="2" />
|
||||
<ListView x:Name="sourceList" Grid.Row="6" >
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Identifier}" FontWeight="Bold" />
|
||||
<TextBlock Text=" (" />
|
||||
<TextBlock Text="{Binding CountryCode}" FontWeight="Bold" />
|
||||
<TextBlock Text=")" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Format}" />
|
||||
<TextBlock Text=" - " />
|
||||
<TextBlock Text="{Binding Carrier}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<Grid Grid.Row="6" Grid.Column="1" DataContext="{Binding SelectedItem, ElementName=sourceList}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Identifier}" FontWeight="Bold" FontSize="14" />
|
||||
<TextBlock Text="{Binding CountryCode, StringFormat='Country code: {0}'}" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Location: " />
|
||||
<TextBlock Text="{Binding Latitude}" />
|
||||
<TextBlock Text="," />
|
||||
<TextBlock Text="{Binding Longitude}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Carrier, StringFormat='Carrier: {0}'}" />
|
||||
<TextBlock Text="{Binding Format, StringFormat='Format: {0}'}" />
|
||||
<TextBlock Text="{Binding FormatDetails, StringFormat='Format details: {0}'}" />
|
||||
<TextBlock Text="{Binding Mountpoint, StringFormat='Mount point: {0}'}" />
|
||||
<TextBlock Text="{Binding Network, StringFormat='Network: {0}'}" />
|
||||
<TextBlock Text="{Binding SupportsNmea, StringFormat='Supports NMEA: {0}'}" />
|
||||
<Button Content="Connect" Click="Connect_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<TextBlock Text="Not connected" x:Name="ntripstatus" Grid.Column="1" Grid.Row="9" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
92
src/SampleApp.WinDesktop/NtripView.xaml.cs
Normal file
92
src/SampleApp.WinDesktop/NtripView.xaml.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using NmeaParser.Gnss.Ntrip;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace SampleApp.WinDesktop
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for NtripView.xaml
|
||||
/// </summary>
|
||||
public partial class NtripView : UserControl
|
||||
{
|
||||
public NtripView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
NmeaParser.Gnss.Ntrip.Client client;
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
sourceList.ItemsSource = null;
|
||||
if (!int.TryParse(port.Text, out int portNumber))
|
||||
{
|
||||
MessageBox.Show("Invalid port number");
|
||||
return;
|
||||
}
|
||||
client = new NmeaParser.Gnss.Ntrip.Client(host.Text, portNumber, username.Text, password.Password);
|
||||
List<NtripStream> sources;
|
||||
try
|
||||
{
|
||||
sources = client.GetSourceTable().OfType<NtripStream>().ToList();
|
||||
}
|
||||
catch(System.Exception ex)
|
||||
{
|
||||
MessageBox.Show("Failed to connect: " + ex.Message);
|
||||
return;
|
||||
}
|
||||
sourceList.ItemsSource = sources.OrderBy(s=>s.CountryCode);
|
||||
}
|
||||
Stream ntripStream;
|
||||
private void Connect_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var streaminfo = ((Button)sender).DataContext as NtripStream;
|
||||
if (streaminfo == null)
|
||||
return;
|
||||
ntripStream?.Dispose();
|
||||
var stream = ntripStream = client.OpenStream(streaminfo.Mountpoint);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
byte[] buffer = new byte[1024];
|
||||
int count = 0;
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
count = await stream.ReadAsync(buffer).ConfigureAwait(false);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
if(!stream.CanRead)
|
||||
{
|
||||
// TODO: Restart stream
|
||||
return;
|
||||
}
|
||||
}
|
||||
var device = MainWindow.currentDevice;
|
||||
if (device != null && device.CanWrite)
|
||||
{
|
||||
await device.WriteAsync(buffer, 0, count).ConfigureAwait(false);
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ntripstatus.Text = $"Transmitted {ntripStream.Position} bytes";
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
ntripstatus.Text = $"Connected";
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/SampleApp.WinDesktop/PointPlotView.xaml
Normal file
66
src/SampleApp.WinDesktop/PointPlotView.xaml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<UserControl x:Class="SampleApp.WinDesktop.PointPlotView"
|
||||
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"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="GridLinesBrush" Color="#AAFFFFFF" />
|
||||
<DoubleCollection x:Key="GridLinesDash">4 4</DoubleCollection>
|
||||
<sys:Double x:Key="GridLinesWidth">.5</sys:Double>
|
||||
</UserControl.Resources>
|
||||
<Grid SizeChanged="Grid_SizeChanged" Background="Black" >
|
||||
<Grid Margin="10" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" x:Name="PlotMap" SizeChanged="PlotMap_SizeChanged">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="50" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="50" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="50" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="50" />
|
||||
</Grid.RowDefinitions>
|
||||
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.ColumnSpan="8" Grid.RowSpan="8" Grid.Row="1" Grid.Column="1" StrokeDashArray="{StaticResource GridLinesDash}" x:Name="OuterRing" />
|
||||
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.ColumnSpan="6" Grid.RowSpan="6" Grid.Row="2" Grid.Column="2" StrokeDashArray="{StaticResource GridLinesDash}" />
|
||||
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.ColumnSpan="4" Grid.RowSpan="4" Grid.Row="3" Grid.Column="3" StrokeDashArray="{StaticResource GridLinesDash}" />
|
||||
<Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.ColumnSpan="2" Grid.RowSpan="2" Grid.Row="4" Grid.Column="4" StrokeDashArray="{StaticResource GridLinesDash}" />
|
||||
<Path Data="M0,0 L1,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.RowSpan="8" Grid.ColumnSpan="8" Grid.Row="1" Grid.Column="1" StrokeDashArray="{StaticResource GridLinesDash}" Stretch="Fill" />
|
||||
<Path Data="M0,0 L0,1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Stroke="{StaticResource GridLinesBrush}" StrokeThickness="{StaticResource GridLinesWidth}" Grid.RowSpan="8" Grid.ColumnSpan="8" Grid.Row="1" Grid.Column="1" StrokeDashArray="{StaticResource GridLinesDash}" Stretch="Fill" />
|
||||
<TextBlock x:Name="FirstMeterLabel" Text="5m" Margin="0,15,0,0" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="White" Grid.RowSpan="10" Grid.Column="6" Grid.ColumnSpan="2" Background="#55000000" />
|
||||
<TextBlock x:Name="SecondMeterLabel" Text="10m" Margin="0,15,-10,0" FontSize="12" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="White" Grid.RowSpan="10" Grid.Column="8" Grid.ColumnSpan="1" Background="#55000000" />
|
||||
<TextBlock Text="N" Margin="10" FontSize="9" HorizontalAlignment="Center" VerticalAlignment="Bottom" Foreground="White" Grid.ColumnSpan="10" />
|
||||
<TextBlock Text="S" Margin="10" FontSize="9" HorizontalAlignment="Center" VerticalAlignment="Top" Foreground="White" Grid.ColumnSpan="10" Grid.Row="9" />
|
||||
<TextBlock Text="W" Margin="10" FontSize="9" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="White" Grid.RowSpan="10" Grid.Column="0" />
|
||||
<TextBlock Text="E" Margin="10" FontSize="9" HorizontalAlignment="Left" VerticalAlignment="Center" Foreground="White" Grid.RowSpan="10" Grid.Column="9" />
|
||||
<Canvas Grid.RowSpan="10" Grid.ColumnSpan="10" x:Name="canvas" Opacity="0">
|
||||
|
||||
</Canvas>
|
||||
<Image x:Name="plot" Grid.RowSpan="8" Grid.ColumnSpan="8" Grid.Row="1" Grid.Column="1" MouseWheel="plot_MouseWheel" />
|
||||
</Grid>
|
||||
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="20" Orientation="Horizontal">
|
||||
<Button Content="Clear" Padding="2" Margin="5" Click="ClearButton_Click" />
|
||||
<ToggleButton IsChecked="True" x:Name="autoFitToggle" Content="Autofit" Padding="2" Margin="5" Unchecked="ToggleButton_Unchecked" Checked="ToggleButton_Checked" />
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Top" Margin="20">
|
||||
<TextBlock x:Name="Status" Foreground="White" FontSize="12" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
302
src/SampleApp.WinDesktop/PointPlotView.xaml.cs
Normal file
302
src/SampleApp.WinDesktop/PointPlotView.xaml.cs
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
using Esri.ArcGISRuntime.Location;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Transactions;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace SampleApp.WinDesktop
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for PointPlotView.xaml
|
||||
/// </summary>
|
||||
public partial class PointPlotView : UserControl
|
||||
{
|
||||
private List<Point> locations = new List<Point>();
|
||||
|
||||
public PointPlotView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Grid_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
PlotMap.Width = PlotMap.Height = Math.Min(e.NewSize.Width, e.NewSize.Height);
|
||||
}
|
||||
Size size = new Size();
|
||||
private void PlotMap_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
size = e.NewSize;
|
||||
UpdatePlot();
|
||||
}
|
||||
|
||||
private void ClearButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
locations.Clear();
|
||||
if(!autoFit)
|
||||
autoFitToggle.IsChecked = true;
|
||||
else
|
||||
UpdatePlot();
|
||||
}
|
||||
|
||||
private struct Point
|
||||
{
|
||||
public double Latitude { get ;set; }
|
||||
public double Longitude { get ;set; }
|
||||
public double Z { get ;set; }
|
||||
public NmeaParser.Messages.Gga.FixQuality Quality { get; set; }
|
||||
}
|
||||
|
||||
public void AddLocation(double latitude, double longitude, double altitude, NmeaParser.Messages.Gga.FixQuality quality)
|
||||
{
|
||||
if(quality == NmeaParser.Messages.Gga.FixQuality.Invalid)
|
||||
return;
|
||||
locations.Add(new Point {
|
||||
Latitude = latitude,
|
||||
Longitude = longitude,
|
||||
Z = altitude,
|
||||
Quality = quality
|
||||
});
|
||||
UpdatePlot();
|
||||
}
|
||||
|
||||
|
||||
private void UpdatePlot()
|
||||
{
|
||||
if (size.Width == 0 || size.Height == 0 || !IsVisible)
|
||||
return;
|
||||
if (locations.Count == 0)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
Status.Text = "";
|
||||
plot.Source = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
var latAvr = locations.Select(l => l.Latitude).Average();
|
||||
var lonAvr = locations.Select(l => l.Longitude).Average();
|
||||
|
||||
//List<double> lonDistances = new List<double>(locations.Count);
|
||||
//List<double> latDistances = new List<double>(locations.Count);
|
||||
List<double> distances = new List<double>(locations.Count);
|
||||
var locations2 = new List<Point>();
|
||||
foreach (var l in locations)
|
||||
{
|
||||
var d = Vincenty.GetDistanceVincenty(latAvr, lonAvr, l.Latitude, l.Longitude);
|
||||
var dLat = Vincenty.GetDistanceVincenty(latAvr, lonAvr, l.Latitude, lonAvr);
|
||||
var dLon = Vincenty.GetDistanceVincenty(latAvr, lonAvr, latAvr, l.Longitude);
|
||||
distances.Add(d);
|
||||
//latDistances.Add(dLat);
|
||||
//lonDistances.Add(dLon);
|
||||
if (latAvr > l.Latitude) dLat = -dLat;
|
||||
if (lonAvr > l.Longitude) dLon = -dLon;
|
||||
locations2.Add(new Point() { Latitude = dLat, Longitude= dLon, Quality = l.Quality });
|
||||
}
|
||||
var latMin = locations2.Select(l => l.Latitude).Min();
|
||||
var lonMin = locations2.Select(l => l.Longitude).Min();
|
||||
var latMax = locations2.Select(l => l.Latitude).Max();
|
||||
var lonMax = locations2.Select(l => l.Longitude).Max();
|
||||
var latAvr2 = locations2.Select(l => l.Latitude).Average();
|
||||
var lonAvr2 = locations2.Select(l => l.Longitude).Average();
|
||||
var maxDifLat = Math.Max(latAvr2 - latMin, latMax - latAvr2);
|
||||
var maxDifLon = Math.Max(lonAvr2 - lonMin, lonMax - lonAvr2);
|
||||
//var maxDif = Math.Max(maxDifLat, maxDifLon);
|
||||
double maxDif = 1;
|
||||
if (autoFit)
|
||||
{
|
||||
maxDif = distances.Max();
|
||||
if (maxDif < 0.05)
|
||||
maxDif = 0.05;
|
||||
else if (maxDif < 1)
|
||||
maxDif = Math.Ceiling(maxDif * 10) / 10d;
|
||||
else
|
||||
maxDif = Math.Ceiling(maxDif);
|
||||
currentScale = maxDif / (Math.Min(size.Width, size.Height) * .5);
|
||||
}
|
||||
else
|
||||
{
|
||||
maxDif = currentScale * (Math.Min(size.Width, size.Height) * .5);
|
||||
}
|
||||
double scale = currentScale;
|
||||
if (scale == 0)
|
||||
scale = 1;
|
||||
int width = (int)size.Width;
|
||||
int height = (int)size.Height;
|
||||
int stride = width * 4;
|
||||
byte[] pixels = new byte[width * height * 4];
|
||||
double[][] stamp = new double[][] {new double[] { .3, .5, .3 }, new double[] { .5, 1, .5 }, new double[] { .3, .5, .3 } };
|
||||
Color col = Colors.Red;
|
||||
for (int i = 0; i < locations2.Count; i++)
|
||||
{
|
||||
var l = locations2[i];
|
||||
var x = (int)(width * .5 + (l.Longitude - lonAvr2) / scale);
|
||||
var y = (int)(height * .5 - (l.Latitude - latAvr2) / scale);
|
||||
var index = ((int)y) * stride + ((int)x) * 4;
|
||||
for (int r = -1; r < stamp.Length-1; r++)
|
||||
{
|
||||
for (int c = -1; c < stamp[r + 1].Length-1; c++)
|
||||
{
|
||||
if (x + c >= width || x + c < 0 ||
|
||||
y + r >= width || y + r < 0)
|
||||
continue;
|
||||
var p = index + r * stride + c * 4;
|
||||
var val = stamp[r + 1][c + 1];
|
||||
switch(l.Quality)
|
||||
{
|
||||
case NmeaParser.Messages.Gga.FixQuality.Estimated:
|
||||
col = Colors.Red; break;
|
||||
case NmeaParser.Messages.Gga.FixQuality.GpsFix:
|
||||
col = Colors.Orange; break;
|
||||
case NmeaParser.Messages.Gga.FixQuality.DgpsFix:
|
||||
col = Colors.Yellow; break;
|
||||
case NmeaParser.Messages.Gga.FixQuality.FloatRtk:
|
||||
col = Color.FromRgb(0,255,0); break;
|
||||
case NmeaParser.Messages.Gga.FixQuality.Rtk:
|
||||
col = Colors.LightBlue; break;
|
||||
default:
|
||||
col = Colors.Gray; break;
|
||||
}
|
||||
pixels[p + 1] = col.B;
|
||||
pixels[p + 1] = col.G;
|
||||
pixels[p + 2] = col.R;
|
||||
pixels[p + 3] = (byte)Math.Min(255, pixels[p + 3] + val * 255); //Multiply alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
var stdDevLat = Math.Sqrt(locations2.Sum(d => (d.Latitude - latAvr2) * (d.Latitude - latAvr2)) / locations2.Count);
|
||||
var stdDevLon = Math.Sqrt(locations2.Sum(d => (d.Longitude - lonAvr2) * (d.Longitude - lonAvr2)) / locations2.Count);
|
||||
var zAvr = locations.Select(l => l.Z).Where(l => !double.IsNaN(l)).Average();
|
||||
var stdDevZ = Math.Sqrt(locations.Select(l => l.Z).Where(l => !double.IsNaN(l)).Sum(d => (d - zAvr) * (d - zAvr)) / locations.Select(l => l.Z).Where(l => !double.IsNaN(l)).Count());
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
SecondMeterLabel.Text = $"{maxDif.ToString("0.###")}m";
|
||||
FirstMeterLabel.Text = $"{(maxDif / 2).ToString("0.###")}m";
|
||||
// Specify the area of the bitmap that changed.
|
||||
var writeableBitmap = new WriteableBitmap((int)size.Width, (int)size.Height, 96, 96, PixelFormats.Bgra32, null);
|
||||
writeableBitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
|
||||
plot.Source = writeableBitmap;
|
||||
Status.Text = $"Measurements: {locations.Count}\nAverage:\n - Latitude: {latAvr.ToString("0.0000000")}\n - Longitude: {lonAvr.ToString("0.0000000")}\n - Elevation: {zAvr.ToString("0.000")}m\nStandard Deviation:\n - Latitude: {stdDevLat.ToString("0.###")}m\n - Longitude: {stdDevLon.ToString("0.###")}m\n - Horizontal: {distances.Average().ToString("0.###")}m\n - Elevation: {stdDevZ.ToString("0.###")}m";
|
||||
});
|
||||
}
|
||||
|
||||
internal static class Vincenty
|
||||
{
|
||||
private const double D2R = 0.01745329251994329576923690768489; //Degrees to radians
|
||||
|
||||
public static double GetDistanceVincenty(double lat1, double lon1, double lat2, double lon2)
|
||||
{
|
||||
return GetDistanceVincenty(lat1, lon1, lat2, lon2, 6378137, 6356752.31424518); //Uses WGS84 values
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vincenty's formulae is used in geodesy to calculate the distance
|
||||
/// between two points on the surface of a spheroid.
|
||||
/// </summary>
|
||||
public static double GetDistanceVincenty(double lat1, double lon1, double lat2, double lon2, double semiMajor, double semiMinor)
|
||||
{
|
||||
var a = semiMajor;
|
||||
var b = semiMinor;
|
||||
var f = (a - b) / a; //flattening
|
||||
var L = (lon2 - lon1) * D2R;
|
||||
var U1 = Math.Atan((1 - f) * Math.Tan(lat1 * D2R));
|
||||
var U2 = Math.Atan((1 - f) * Math.Tan(lat2 * D2R));
|
||||
var sinU1 = Math.Sin(U1);
|
||||
var cosU1 = Math.Cos(U1);
|
||||
var sinU2 = Math.Sin(U2);
|
||||
var cosU2 = Math.Cos(U2);
|
||||
|
||||
double lambda = L;
|
||||
double lambdaP;
|
||||
double cosSigma, cosSqAlpha, sinSigma, cos2SigmaM, sigma, sinLambda, cosLambda;
|
||||
|
||||
int iterLimit = 100;
|
||||
do
|
||||
{
|
||||
sinLambda = Math.Sin(lambda);
|
||||
cosLambda = Math.Cos(lambda);
|
||||
sinSigma = Math.Sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) +
|
||||
(cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));
|
||||
if (sinSigma == 0)
|
||||
return 0; // co-incident points
|
||||
|
||||
cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
|
||||
sigma = Math.Atan2(sinSigma, cosSigma);
|
||||
double sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
|
||||
cosSqAlpha = 1 - sinAlpha * sinAlpha;
|
||||
cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;
|
||||
if (double.IsNaN(cos2SigmaM))
|
||||
cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6)
|
||||
double C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));
|
||||
lambdaP = lambda;
|
||||
lambda = L + (1 - C) * f * sinAlpha *
|
||||
(sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)));
|
||||
} while (Math.Abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0);
|
||||
|
||||
if (iterLimit == 0) return double.NaN; // formula failed to converge
|
||||
|
||||
var uSq = cosSqAlpha * (a * a - b * b) / (b * b);
|
||||
var A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
|
||||
var B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
|
||||
var deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) -
|
||||
B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
|
||||
var s = b * A * (sigma - deltaSigma);
|
||||
|
||||
s = Math.Round(s, 3); // round to 1mm precision
|
||||
return s;
|
||||
}
|
||||
}
|
||||
bool autoFit = true;
|
||||
double currentScale = 1;
|
||||
private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
autoFit = false;
|
||||
}
|
||||
|
||||
private void ToggleButton_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
autoFit = true;
|
||||
UpdatePlot();
|
||||
}
|
||||
|
||||
private void plot_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
var maxDif = Math.Round(currentScale * (Math.Min(size.Width, size.Height) * .5), 3);
|
||||
var dif = 1;
|
||||
if (maxDif < 1)
|
||||
maxDif = e.Delta < 0 ? maxDif * 2 : maxDif / 2;
|
||||
else
|
||||
maxDif = e.Delta < 0 ? maxDif + 1 : maxDif - 1;
|
||||
if (maxDif < 0.05)
|
||||
maxDif = 0.05;
|
||||
else if (maxDif < 1)
|
||||
maxDif = Math.Ceiling(maxDif * 10) / 10d;
|
||||
else
|
||||
maxDif = Math.Ceiling(maxDif);
|
||||
currentScale = maxDif / (Math.Min(size.Width, size.Height) * .5);
|
||||
|
||||
if (autoFitToggle.IsChecked == true)
|
||||
{
|
||||
autoFitToggle.IsChecked = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdatePlot();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
xmlns:local="clr-namespace:SampleApp.WinDesktop"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="300">
|
||||
<Grid Height="100">
|
||||
<Grid Height="125">
|
||||
|
||||
<Grid >
|
||||
<Grid.RowDefinitions>
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
Stretch="Fill"
|
||||
Data="M0,0 L 1,0 M 0,1 L 1,1 M 0,2 L 1,2 M 0,3 L 1,3 M 0,4 L 1,4" />
|
||||
</Grid>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto">
|
||||
<ItemsControl x:Name="satellites"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
|
|
@ -37,6 +38,7 @@
|
|||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.Resources>
|
||||
<local:SatelliteVechicleColorConverter x:Key="conv" />
|
||||
<local:SnrToHeightConverter x:Key="heightconv" />
|
||||
</ItemsControl.Resources>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
|
@ -48,7 +50,7 @@
|
|||
<SolidColorBrush Color="{Binding Converter={StaticResource conv}}" />
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
<Border Height="{Binding SignalToNoiseRatio, FallbackValue=10}"
|
||||
<Border Height="{Binding Converter={StaticResource heightconv}}"
|
||||
BorderBrush="Black"
|
||||
Margin="5,0" Width="20"
|
||||
BorderThickness="1"
|
||||
|
|
@ -62,5 +64,6 @@
|
|||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using System;
|
||||
using NmeaParser;
|
||||
using NmeaParser.Messages;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -24,23 +27,37 @@ namespace SampleApp.WinDesktop
|
|||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public NmeaParser.Messages.Gsv GsvMessage
|
||||
Dictionary<Talker, Gsv> messages = new Dictionary<Talker, Gsv>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
public class SnrToHeightConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if(value is SatelliteVehicle sv)
|
||||
{
|
||||
return Math.Max(10, sv.SignalToNoiseRatio * 2);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,23 +27,21 @@ namespace SampleApp.WinDesktop
|
|||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public Gsv GsvMessage
|
||||
Dictionary<Talker, Gsv> messages = new Dictionary<Talker, Gsv>();
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
|
@ -48,7 +49,7 @@ namespace SampleApp.WinDesktop
|
|||
List<string> values = new List<string>();
|
||||
foreach (var prop in props.OrderBy(t => t.Name))
|
||||
{
|
||||
if (prop.Name == nameof(NmeaMessage.MessageType) || prop.Name == nameof(NmeaMessage.Checksum))
|
||||
if (prop.Name == nameof(NmeaMessage.MessageType) || prop.Name == nameof(NmeaMessage.Checksum) || prop.Name == nameof(NmeaMessage.Timestamp))
|
||||
continue;
|
||||
var value = prop.GetValue(e.NewValue);
|
||||
if (!(value is string) && value is System.Collections.IEnumerable arr)
|
||||
|
|
@ -60,6 +61,13 @@ namespace SampleApp.WinDesktop
|
|||
}
|
||||
values.Add($"{prop.Name}: {value}");
|
||||
}
|
||||
if (e.NewValue is NmeaMessage msg)
|
||||
{
|
||||
var age = (System.Diagnostics.Stopwatch.GetTimestamp() * 1000d / System.Diagnostics.Stopwatch.Frequency) - msg.Timestamp;
|
||||
values.Add($"Timestamp: " + DateTime.Now.AddMilliseconds(-age).TimeOfDay.ToString("h\\:mm\\:ss"));
|
||||
}
|
||||
//;
|
||||
|
||||
ctrl.Values.ItemsSource = values;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<!--Map-->
|
||||
<esri:MapView x:Name="mapView" Grid.Row="1">
|
||||
<local:RestoreAutoPanMode.RestoreAutoPanSettings>
|
||||
<local:RestoreAutoPanMode DelayInSeconds="2.5" PanMode="Navigation" IsEnabled="True" RestoreScale="5000" />
|
||||
<local:RestoreAutoPanMode DelayInSeconds="2.5" PanMode="Recenter" IsEnabled="True" RestoreScale="5000" />
|
||||
</local:RestoreAutoPanMode.RestoreAutoPanSettings>
|
||||
</esri:MapView>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,13 @@ namespace SampleApp.WinDesktop
|
|||
}
|
||||
|
||||
mapView.LocationDisplay.InitialZoomScale = 5000;
|
||||
mapView.LocationDisplay.AutoPanMode = Esri.ArcGISRuntime.UI.LocationDisplayAutoPanMode.Navigation;
|
||||
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 NmeaLocationProvider(newDevice);
|
||||
mapView.LocationDisplay.IsEnabled = true;
|
||||
mapView.LocationDisplay.DataSource = new NmeaLocationDataSource(newDevice, false);
|
||||
mapView.LocationDisplay.IsEnabled = IsLoaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<PropertyGroup>
|
||||
<TargetFrameworks>net451;netcoreapp3.1</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@
|
|||
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<PackageCertificateKeyFile>NmeaParser.Tests.UWP_TemporaryKey.pfx</PackageCertificateKeyFile>
|
||||
<UnitTestPlatformVersion Condition="'$(UnitTestPlatformVersion)' == ''">$(VisualStudioVersion)</UnitTestPlatformVersion>
|
||||
<PackageCertificateThumbprint>D07B149B4E796AB0184B2E2FC9DDC1A2F5CA5A7E</PackageCertificateThumbprint>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageCertificateThumbprint>59902E8FFCA0C735A9FB2184201D87DF7DBF401B</PackageCertificateThumbprint>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -97,7 +97,33 @@ namespace NmeaParser.Tests
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void MissingChecksumAfterStar()
|
||||
{
|
||||
string input = "$GPRMA,A,4917.24,S,12309.57,W,1000.0,2000.0,123.4,321.0,10,E,A*";
|
||||
var msg = NmeaMessage.Parse(input);
|
||||
Assert.IsNotNull(msg);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MissingChecksum()
|
||||
{
|
||||
string input = "$GPRMA,A,4917.24,S,12309.57,W,1000.0,2000.0,123.4,321.0,10,E,A";
|
||||
var msg = NmeaMessage.Parse(input);
|
||||
Assert.IsNotNull(msg);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IgnoreChecksum()
|
||||
{
|
||||
string input = "$GPRMA,A,4917.24,S,12309.57,W,1000.0,2000.0,123.4,321.0,10,E,A*00";
|
||||
var msg = NmeaMessage.Parse(input, ignoreChecksum: true);
|
||||
Assert.IsNotNull(msg);
|
||||
|
||||
Assert.ThrowsException<ArgumentException>(() => NmeaMessage.Parse(input, ignoreChecksum: false));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestGprma()
|
||||
{
|
||||
|
|
@ -201,7 +227,7 @@ namespace NmeaParser.Tests
|
|||
Assert.AreEqual(-22.1, gga.GeoidalSeparation);
|
||||
Assert.AreEqual("M", gga.GeoidalSeparationUnits);
|
||||
Assert.AreEqual(-1, gga.DgpsStationId);
|
||||
Assert.AreEqual(TimeSpan.MaxValue, gga.TimeSinceLastDgpsUpdate);
|
||||
Assert.IsNull(gga.TimeSinceLastDgpsUpdate);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
@ -516,7 +542,7 @@ namespace NmeaParser.Tests
|
|||
Assert.AreEqual(1, gns.ModeIndicators.Length);
|
||||
Assert.AreEqual(19, gns.NumberOfSatellites);
|
||||
Assert.AreEqual(.6, gns.Hdop);
|
||||
Assert.AreEqual(406.110, gns.OrhometricHeight);
|
||||
Assert.AreEqual(406.110, gns.OrthometricHeight);
|
||||
Assert.AreEqual(-26.294, gns.GeoidalSeparation);
|
||||
Assert.AreEqual("0138", gns.DgpsStationId);
|
||||
Assert.AreEqual(Gns.NavigationalStatus.Safe, gns.Status);
|
||||
|
|
@ -542,7 +568,7 @@ namespace NmeaParser.Tests
|
|||
Assert.AreEqual(0, gns.ModeIndicators.Length);
|
||||
Assert.AreEqual(6, gns.NumberOfSatellites);
|
||||
Assert.AreEqual(double.NaN, gns.Hdop);
|
||||
Assert.AreEqual(double.NaN, gns.OrhometricHeight);
|
||||
Assert.AreEqual(double.NaN, gns.OrthometricHeight);
|
||||
Assert.AreEqual(double.NaN, gns.GeoidalSeparation);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(2), gns.TimeSinceLastDgpsUpdate);
|
||||
Assert.AreEqual("0", gns.DgpsStationId);
|
||||
|
|
@ -573,9 +599,9 @@ namespace NmeaParser.Tests
|
|||
Assert.AreEqual(Gns.Mode.NoFix, gns.ModeIndicators[4]);
|
||||
Assert.AreEqual(10, gns.NumberOfSatellites);
|
||||
Assert.AreEqual(1.4, gns.Hdop);
|
||||
Assert.AreEqual(402.411, gns.OrhometricHeight);
|
||||
Assert.AreEqual(402.411, gns.OrthometricHeight);
|
||||
Assert.AreEqual(-32.133, gns.GeoidalSeparation);
|
||||
Assert.AreEqual(TimeSpan.MaxValue, gns.TimeSinceLastDgpsUpdate);
|
||||
Assert.IsNull(gns.TimeSinceLastDgpsUpdate);
|
||||
Assert.AreEqual(null, gns.DgpsStationId);
|
||||
Assert.AreEqual(Gns.NavigationalStatus.NotValid, gns.Status);
|
||||
}
|
||||
|
|
@ -621,7 +647,7 @@ namespace NmeaParser.Tests
|
|||
Assert.AreEqual(0, gns.ModeIndicators.Length);
|
||||
Assert.AreEqual(4, gns.NumberOfSatellites);
|
||||
Assert.AreEqual(double.NaN, gns.Hdop);
|
||||
Assert.AreEqual(double.NaN, gns.OrhometricHeight);
|
||||
Assert.AreEqual(double.NaN, gns.OrthometricHeight);
|
||||
Assert.AreEqual(double.NaN, gns.GeoidalSeparation);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(2), gns.TimeSinceLastDgpsUpdate);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue