//
// Copyright (c) 2014 Morten Nielsen
//
// Licensed under the Microsoft Public License (Ms-PL) (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://opensource.org/licenses/Ms-PL.html
//
// 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 System.Text;
using System.Threading.Tasks;
using System.IO;
using Windows.Foundation;
namespace NmeaParser
{
///
/// A generic abstract NMEA device
///
public abstract class NmeaDevice : IDisposable
{
private object m_lockObject = new object();
private string m_message = "";
private Stream m_stream;
System.Threading.CancellationTokenSource m_cts;
TaskCompletionSource closeTask;
///
/// Initializes a new instance of the class.
///
protected NmeaDevice()
{
}
///
/// Opens the device connection.
///
///
public async Task OpenAsync()
{
lock (m_lockObject)
{
if (IsOpen) return;
IsOpen = true;
}
m_cts = new System.Threading.CancellationTokenSource();
m_stream = await OpenStreamAsync();
StartParser();
MultiPartMessageCache.Clear();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "_")]
private void StartParser()
{
var token = m_cts.Token;
System.Diagnostics.Debug.WriteLine("Starting parser...");
var _ = Task.Run(async () =>
{
var stream = m_stream;
byte[] buffer = new byte[1024];
while (!token.IsCancellationRequested)
{
int readCount = 0;
try
{
readCount = await stream.ReadAsync(buffer, 0, 1024, token).ConfigureAwait(false);
}
catch { }
if (token.IsCancellationRequested)
break;
if (readCount > 0)
{
OnData(buffer.Take(readCount).ToArray());
}
await Task.Delay(10, token);
}
if (closeTask != null)
closeTask.SetResult(true);
});
}
///
/// Creates the stream the NmeaDevice is working on top off.
///
///
protected abstract Task OpenStreamAsync();
///
/// Closes the device.
///
///
public async Task CloseAsync()
{
if (m_cts != null)
{
closeTask = new TaskCompletionSource();
if (m_cts != null)
m_cts.Cancel();
m_cts = null;
}
await closeTask.Task;
await CloseStreamAsync(m_stream);
MultiPartMessageCache.Clear();
m_stream = null;
lock (m_lockObject)
IsOpen = false;
}
///
/// Closes the stream the NmeaDevice is working on top off.
///
/// The stream.
///
protected abstract Task CloseStreamAsync(Stream stream);
private void OnData(byte[] data)
{
var nmea = System.Text.Encoding.UTF8.GetString(data, 0, data.Length);
string line = null;
lock (m_lockObject)
{
m_message += nmea;
var lineEnd = m_message.IndexOf("\n", StringComparison.Ordinal);
if (lineEnd > -1)
{
line = m_message.Substring(0, lineEnd).Trim();
m_message = m_message.Substring(lineEnd + 1);
}
}
if (!string.IsNullOrEmpty(line))
ProcessMessage(line);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification="Must silently handle invalid/corrupt input")]
private void ProcessMessage(string p)
{
try
{
var msg = NmeaParser.Nmea.NmeaMessage.Parse(p);
if (msg != null)
OnMessageReceived(msg);
}
catch { }
}
private void OnMessageReceived(Nmea.NmeaMessage msg)
{
var args = new NmeaMessageReceivedEventArgs(msg);
var multi = msg as IMultiPartMessage;
if (multi != null)
{
args.IsMultipart = true;
if (MultiPartMessageCache.ContainsKey(msg.MessageType))
{
var dic = MultiPartMessageCache[msg.MessageType];
if (dic.ContainsKey(multi.MessageNumber - 1) && !dic.ContainsKey(multi.MessageNumber))
{
dic[multi.MessageNumber] = msg;
}
else //Something is out of order. Clear cache
MultiPartMessageCache.Remove(msg.MessageType);
}
else if (multi.MessageNumber == 1)
{
MultiPartMessageCache[msg.MessageType] = new Dictionary(multi.TotalMessages);
MultiPartMessageCache[msg.MessageType][1] = msg;
}
if (MultiPartMessageCache.ContainsKey(msg.MessageType))
{
var dic = MultiPartMessageCache[msg.MessageType];
if (dic.Count == multi.TotalMessages) //We have a full list
{
MultiPartMessageCache.Remove(msg.MessageType);
args.MessageParts = dic.Values.ToArray();
}
}
}
if (MessageReceived != null)
{
MessageReceived(this, args);
}
}
private Dictionary> MultiPartMessageCache
= new Dictionary>();
///
/// Occurs when an NMEA message is received.
///
public event EventHandler MessageReceived;
///
/// Releases unmanaged and - optionally - managed resources.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases unmanaged and - optionally - managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
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;
}
}
///
/// Gets a value indicating whether this device is open.
///
///
/// true if this instance is open; otherwise, false.
///
public bool IsOpen { get; private set; }
}
///
/// Event argument for the
///
public sealed class NmeaMessageReceivedEventArgs : EventArgs
{
internal NmeaMessageReceivedEventArgs(Nmea.NmeaMessage message) {
Message = message;
}
///
/// Gets the nmea message.
///
///
/// The nmea message.
///
public Nmea.NmeaMessage Message { get; private set; }
///
/// Gets a value indicating whether this instance is a multi part message.
///
///
/// true if this instance is multi part; otherwise, false.
///
public bool IsMultipart { get; internal set; }
///
/// Gets the message parts if this is a multi-part message and all message parts has been received.
///
///
/// The message parts.
///
public IReadOnlyList MessageParts { get; internal set; }
}
}