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