Merge remote-tracking branch 'origin/main' into docfx_monikers

This commit is contained in:
Morten Nielsen 2020-08-25 13:16:30 -07:00
commit 8583c54017
54 changed files with 2880 additions and 564 deletions

View file

@ -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;
}
}
```
![Screenshot](https://user-images.githubusercontent.com/1378165/73328707-95990e80-420f-11ea-85a7-43149e29bd21.png)

View file

@ -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

View file

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

View file

@ -0,0 +1,305 @@
// *******************************************************************************
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// ******************************************************************************
using System;
using System.Collections.Generic;
using System.Linq;
using NmeaParser.Messages;
namespace NmeaParser.Gnss
{
/// <summary>
/// Helper class for monitoring GNSS messages and combine them into a single useful location info
/// </summary>
public class GnssMonitor
{
private bool m_supportGNMessages; // If device detect GN* messages, ignore all other Talker ID
private bool m_supportGGaMessages; //If device support GGA, ignore RMC for location
/// <summary>
/// Initializes a new instance of the <see cref="GnssMonitor"/> class.
/// </summary>
/// <param name="device">The NMEA device to monitor for GNSS messages</param>
public GnssMonitor(NmeaDevice device)
{
if (device == null)
throw new ArgumentNullException(nameof(device));
Device = device;
Device.MessageReceived += NmeaMessageReceived;
}
/// <summary>
/// Gets the NMEA device that is being monitored
/// </summary>
public NmeaDevice Device { get; }
private void NmeaMessageReceived(object sender, NmeaParser.NmeaMessageReceivedEventArgs e)
{
OnMessageReceived(e.Message);
}
/// <summary>
/// Called when a message is received.
/// </summary>
/// <param name="message">The NMEA message that was received</param>
protected virtual void OnMessageReceived(NmeaMessage message)
{
bool isNewFix = false;
bool lostFix = false;
double lat = 0;
double lon = 0;
AllMessages[message.MessageType] = message;
if (message.TalkerId == NmeaParser.Talker.GlobalNavigationSatelliteSystem)
m_supportGNMessages = true; // Support for GN* messages detected
else if (m_supportGNMessages && message.TalkerId != NmeaParser.Talker.GlobalNavigationSatelliteSystem)
return; // If device supports combined GN* messages, ignore non-GN messages
if (message is 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;
}
}

View 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
}
}

View 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; }
}
}

View 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; }
}
}
}

View 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()
{
}
}
}

View 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
View 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
View 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; }
}
}

View file

@ -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>

View file

@ -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>

View file

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

View file

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

View file

@ -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>

View 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; }
}
}

View 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; }
}
}

View file

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

View file

@ -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

View file

@ -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,

View file

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

View 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; }
}
}

View file

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

View file

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

View file

@ -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>

View file

@ -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
{

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,28 @@
<UserControl x:Class="SampleApp.WinDesktop.GnssMonitorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SampleApp.WinDesktop"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel DataContext="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=Message}">
<Border Background="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>

View file

@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using NmeaParser.Gnss;
using NmeaParser.Messages;
namespace SampleApp.WinDesktop
{
/// <summary>
/// Interaction logic for GnssMonitorView.xaml
/// </summary>
public partial class GnssMonitorView : UserControl
{
public GnssMonitorView()
{
InitializeComponent();
}
public GnssMonitor Monitor
{
get { return (GnssMonitor)GetValue(MonitorProperty); }
set { SetValue(MonitorProperty, value); }
}
public static readonly DependencyProperty MonitorProperty =
DependencyProperty.Register(nameof(Monitor), typeof(GnssMonitor), typeof(GnssMonitorView), new PropertyMetadata(null, (d,e) => ((GnssMonitorView)d).OnMonitorPropertyChanged(e)));
private void OnMonitorPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is GnssMonitor oldMonitor)
{
oldMonitor.LocationChanged -= LocationChanged;
oldMonitor.LocationLost -= LocationChanged;
}
if (e.NewValue is GnssMonitor newMonitor)
{
newMonitor.LocationChanged += LocationChanged;
newMonitor.LocationLost += LocationChanged;
}
UpdateValues();
}
private void LocationChanged(object sender, System.EventArgs e)
{
Dispatcher.Invoke(UpdateValues);
}
private void UpdateValues()
{
if (Monitor == null)
Values.ItemsSource = null;
else
{
var props = Monitor.GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
List<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>();
foreach (var prop in props.OrderBy(t => t.Name))
{
if (prop.Name == nameof(GnssMonitor.AllMessages)) continue;
if (prop.PropertyType.IsSubclassOf(typeof(NmeaMessage)))
continue;
var value = prop.GetValue(Monitor);
if (!(value is string) && value is System.Collections.IEnumerable arr)
{
var str = "[" + string.Join(",", arr.OfType<object>().ToArray()) + "]";
if (str.Length == 2)
str = "[ ]";
value = str;
}
values.Add(new KeyValuePair<string, object>(prop.Name, value));
}
Values.ItemsSource = values;
}
}
}
}

View file

@ -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}" />

View file

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

View file

@ -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>

View file

@ -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

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

View file

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

View 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>

View 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";
}
}
}

View 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>

View 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();
}
}
}
}

View file

@ -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>

View file

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

View file

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

View file

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

View file

@ -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>

View file

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

View file

@ -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)
{

View file

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net451;netcoreapp3.1</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>

View file

@ -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>

View file

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