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