From 2e227a65d88ec652c5011a171cd9297fac4be75d Mon Sep 17 00:00:00 2001
From: The6P4C <watsonjcampbell@gmail.com>
Date: Fri, 29 Sep 2017 18:11:24 +1000
Subject: [PATCH] Add NMEA device support

---
 GPSDOTimeSync/FormMain.cs                     |  12 ++
 GPSDOTimeSync/GPSDOTimeSync.csproj            |   3 +
 .../TimeProviders/NMEA/NMEASerialPort.cs      | 168 ++++++++++++++++++
 .../TimeProviders/NMEA/NMEATimeProvider.cs    |  50 ++++++
 4 files changed, 233 insertions(+)
 create mode 100644 GPSDOTimeSync/TimeProviders/NMEA/NMEASerialPort.cs
 create mode 100644 GPSDOTimeSync/TimeProviders/NMEA/NMEATimeProvider.cs

diff --git a/GPSDOTimeSync/FormMain.cs b/GPSDOTimeSync/FormMain.cs
index d7f1f83..1b08e56 100644
--- a/GPSDOTimeSync/FormMain.cs
+++ b/GPSDOTimeSync/FormMain.cs
@@ -6,6 +6,9 @@ using System.Windows.Forms;
 using GPSDOTimeSync.Devices.Thunderbolt;
 using GPSDOTimeSync.TimeProviders;
 using GPSDOTimeSync.TimeProviders.Thunderbolt;
+using GPSDOTimeSync.TimeProviders.NMEA;
+using System.Diagnostics;
+using System.Linq;
 
 namespace GPSDOTimeSync {
 	public partial class FormMain : Form {
@@ -22,6 +25,15 @@ namespace GPSDOTimeSync {
 					ThunderboltSerialPort thunderboltSerialPort = new ThunderboltSerialPort(serialPort);
 					ITimeProvider timeProvider = new ThunderboltTimeProvider(thunderboltSerialPort);
 
+					return timeProvider;
+				})
+			},
+			{
+				"NMEA Device (e.g. BG7TBL GPSDO)",
+				new Func<SerialPort, ITimeProvider>((serialPort) => {
+					NMEASerialPort nmeaSerialPort = new NMEASerialPort(serialPort);
+					ITimeProvider timeProvider = new NMEATimeProvider(nmeaSerialPort);
+
 					return timeProvider;
 				})
 			}
diff --git a/GPSDOTimeSync/GPSDOTimeSync.csproj b/GPSDOTimeSync/GPSDOTimeSync.csproj
index b773ba5..00e428f 100644
--- a/GPSDOTimeSync/GPSDOTimeSync.csproj
+++ b/GPSDOTimeSync/GPSDOTimeSync.csproj
@@ -56,6 +56,8 @@
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="SystemTimeUtils.cs" />
     <Compile Include="TimeProviders\ITimeProvider.cs" />
+    <Compile Include="TimeProviders\NMEA\NMEASerialPort.cs" />
+    <Compile Include="TimeProviders\NMEA\NMEATimeProvider.cs" />
     <Compile Include="TimeProviders\Thunderbolt\ThunderboltSerialPort.cs" />
     <Compile Include="TimeProviders\Thunderbolt\ThunderboltTimeProvider.cs" />
     <EmbeddedResource Include="FormMain.resx">
@@ -83,6 +85,7 @@
   <ItemGroup>
     <None Include="App.config" />
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
diff --git a/GPSDOTimeSync/TimeProviders/NMEA/NMEASerialPort.cs b/GPSDOTimeSync/TimeProviders/NMEA/NMEASerialPort.cs
new file mode 100644
index 0000000..ae63935
--- /dev/null
+++ b/GPSDOTimeSync/TimeProviders/NMEA/NMEASerialPort.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections.Generic;
+using System.IO.Ports;
+using System.Text;
+using System.Threading;
+
+namespace GPSDOTimeSync.TimeProviders.NMEA {
+	public class NMEASentence {
+		/// <summary>
+		/// The validity of the NMEA sentence.
+		/// If <code>false</code>, the values of <code>Talker</code>, <code>MessageType</code>, <code>Data</code>, <code>Checksum</code> and <code>RawSentence</code> must not be used.
+		/// If <code>true</code>, the sentence may still contain invalid values.
+		/// </summary>
+		public bool IsSentenceValid { get; }
+
+		/// <summary>
+		/// The talker of the NMEA sentence.
+		/// </summary>
+		public string Talker { get; }
+
+		/// <summary>
+		/// The NMEA message type.
+		/// </summary>
+		public string MessageType { get; }
+
+		/// <summary>
+		/// The data fields of the NMEA message.
+		/// </summary>
+		public List<string> Data { get; }
+
+		/// <summary>
+		/// The checksum of the message, if present.
+		/// If the message did not contain a checksum, <code>Checksum</code> equals -1.
+		/// </summary>
+		public int Checksum { get; }
+
+		/// <summary>
+		/// The raw, unparsed NMEA sentence.
+		/// </summary>
+		public string RawSentence { get; }
+
+		public NMEASentence(bool isSentenceValid, string talker, string messageType, List<string> data, int checksum, string rawSentence) {
+			IsSentenceValid = isSentenceValid;
+
+			Talker = talker;
+			MessageType = messageType;
+			Data = data;
+			Checksum = checksum;
+
+			RawSentence = rawSentence;
+		}
+	}
+
+	class NMEASerialPort {
+		private StringBuilder sentenceBuffer;
+		private bool inSentence;
+
+		private SerialPort serialPort;
+
+		private bool running;
+		private Thread readThread;
+
+		/// <summary>
+		/// A delegate which is used for the <code>SentenceReceived</code> event.
+		/// </summary>
+		/// <param name="sentence">The parsed NMEA sentence which was received.</param>
+		public delegate void SentenceReceivedEventHandler(NMEASentence sentence);
+
+		/// <summary>
+		/// An event which is called when a sentence is received over the serial port.
+		/// </summary>
+		public event SentenceReceivedEventHandler SentenceReceived;
+
+		/// <summary>
+		/// Creates an instance of the NMEASerialPort class, which processes serial data from an NMEA device.
+		/// The serial port passed into the function must not be opened.
+		/// </summary>
+		/// <param name="serialPort">The serial port with which to communicate with the NMEA device.</param>
+		public NMEASerialPort(SerialPort serialPort) {
+			sentenceBuffer = new StringBuilder();
+			inSentence = false;
+
+			this.serialPort = serialPort;
+
+			readThread = new Thread(ReadSerialPort);
+			readThread.Name = "NMEASerialPort Read";
+		}
+
+		/// <summary>
+		/// Begins processing serial data and firing SentenceReceived events.
+		/// </summary>
+		public void Open() {
+			running = true;
+
+			serialPort.Open();
+			readThread.Start();
+		}
+
+		/// <summary>
+		/// Stops processing serial data and firing SentenceReceived events.
+		/// </summary>
+		public void Close() {
+			running = false;
+
+			readThread.Join();
+			serialPort.Close();
+		}
+
+		private void ProcessSentence() {
+			string sentence = sentenceBuffer.ToString();
+
+			List<string> components = new List<string>(sentence.Split(','));
+
+			string talkerAndMessageType = components[0];
+			string talker = talkerAndMessageType.Substring(0, 2);
+			string messageType = talkerAndMessageType.Substring(2);
+
+			List<string> data = components.GetRange(1, components.Count - 1);
+
+			string lastEntry = data[data.Count - 1];
+
+			int checksum = -1;
+
+			// Check if last entry is long enough to have a checksum at the end, and if so,
+			// if there's a star character there
+			if (lastEntry.Length >= 3 && lastEntry[lastEntry.Length - 3] == '*') {
+				// Fix last entry by removing checksum
+				data[data.Count - 1] = lastEntry.Substring(0, lastEntry.Length - 3);
+
+				string checksumString = lastEntry.Substring(lastEntry.Length - 2);
+				checksum = Convert.ToInt32(checksumString, 16);
+			}
+
+			SentenceReceived?.Invoke(new NMEASentence(true, talker, messageType, data, checksum, sentence));
+		}
+
+		private void ProcessByte(byte b) {
+			char c = (char) b;
+
+			if (inSentence) {
+				if (c != '\r') {
+					sentenceBuffer.Append(c);
+				} else {
+					ProcessSentence();
+
+					sentenceBuffer = new StringBuilder();
+					inSentence = false;
+				}
+			} else {
+				if (c == '$') {
+					inSentence = true;
+				}
+			}
+		}
+
+		private void ReadSerialPort() {
+			while (running) {
+				if (serialPort.BytesToRead > 0) {
+					int possibleCurrentByte = serialPort.ReadByte();
+
+					if (possibleCurrentByte != -1) {
+						ProcessByte((byte) possibleCurrentByte);
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/GPSDOTimeSync/TimeProviders/NMEA/NMEATimeProvider.cs b/GPSDOTimeSync/TimeProviders/NMEA/NMEATimeProvider.cs
new file mode 100644
index 0000000..528be9c
--- /dev/null
+++ b/GPSDOTimeSync/TimeProviders/NMEA/NMEATimeProvider.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Diagnostics;
+
+namespace GPSDOTimeSync.TimeProviders.NMEA {
+	class NMEATimeProvider : ITimeProvider {
+		private NMEASerialPort nmeaSerialPort;
+
+		public event TimeAvailableEventHandler TimeAvailable;
+		public event LogEventHandler Log;
+
+		/// <summary>
+		/// Creates an instance of the NMEATimeProvider class, which provides time information through the <code>ITimeProvider</code> interface.
+		/// The <code>NMEASerialPort</code> instance passed into this function must not be open.
+		/// </summary>
+		/// <param name="nmeaSerialPort">The <code>NMEASerialPort</code> instance to use when communicating with the NMEA device.</param>
+		public NMEATimeProvider(NMEASerialPort nmeaSerialPort) {
+			this.nmeaSerialPort = nmeaSerialPort;
+
+			nmeaSerialPort.SentenceReceived += SentenceReceived;
+		}
+
+		public void Start() {
+			nmeaSerialPort.Open();
+		}
+
+		public void Stop() {
+			nmeaSerialPort.Close();
+		}
+
+		private void SentenceReceived(NMEASentence sentence) {
+			if (sentence.IsSentenceValid) {
+				if (sentence.Talker == "GP" && sentence.MessageType == "RMC") {
+					string timeString = sentence.Data[0];
+					int hour = int.Parse(timeString.Substring(0, 2));
+					int minute = int.Parse(timeString.Substring(2, 2));
+					int second = int.Parse(timeString.Substring(4, 2));
+
+					string dateString = sentence.Data[8];
+					int day = int.Parse(dateString.Substring(0, 2));
+					int month = int.Parse(dateString.Substring(2, 2));
+					int year = 2000 + int.Parse(dateString.Substring(4, 2));
+
+					TimeAvailable?.Invoke(new DateTime(year, month, day, hour, minute, second));
+				}
+			} else {
+				Log?.Invoke("An invalid packet was received.", LogLevel.Warning);
+			}
+		}
+	}
+}