diff --git a/ThunderboltTimeSync/FormMain.Designer.cs b/ThunderboltTimeSync/FormMain.Designer.cs index b01657d..d8ef874 100644 --- a/ThunderboltTimeSync/FormMain.Designer.cs +++ b/ThunderboltTimeSync/FormMain.Designer.cs @@ -24,6 +24,9 @@ /// private void InitializeComponent() { this.labelTimestamps = new System.Windows.Forms.Label(); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.latestLogMessage = new System.Windows.Forms.ToolStripStatusLabel(); + this.statusStrip.SuspendLayout(); this.SuspendLayout(); // // labelTimestamps @@ -34,21 +37,44 @@ this.labelTimestamps.Size = new System.Drawing.Size(486, 358); this.labelTimestamps.TabIndex = 0; // + // statusStrip + // + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.latestLogMessage}); + this.statusStrip.Location = new System.Drawing.Point(0, 336); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.Size = new System.Drawing.Size(486, 22); + this.statusStrip.TabIndex = 1; + this.statusStrip.Text = "statusStrip1"; + // + // latestLogMessage + // + this.latestLogMessage.Name = "latestLogMessage"; + this.latestLogMessage.Size = new System.Drawing.Size(66, 17); + this.latestLogMessage.Text = "{RUNTIME}"; + // // FormMain // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(486, 358); + this.Controls.Add(this.statusStrip); this.Controls.Add(this.labelTimestamps); this.Name = "FormMain"; this.Text = "Thunderbolt Time Sync"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormMain_FormClosing); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); this.ResumeLayout(false); + this.PerformLayout(); } #endregion private System.Windows.Forms.Label labelTimestamps; + private System.Windows.Forms.StatusStrip statusStrip; + private System.Windows.Forms.ToolStripStatusLabel latestLogMessage; } } diff --git a/ThunderboltTimeSync/FormMain.cs b/ThunderboltTimeSync/FormMain.cs index 1b185b4..98c484c 100644 --- a/ThunderboltTimeSync/FormMain.cs +++ b/ThunderboltTimeSync/FormMain.cs @@ -1,10 +1,22 @@ using System; -using System.Diagnostics; +using System.Collections.Generic; +using System.Drawing; using System.IO.Ports; using System.Windows.Forms; +using ThunderboltTimeSync.Devices.Thunderbolt; +using ThunderboltTimeSync.TimeProviders; +using ThunderboltTimeSync.TimeProviders.Thunderbolt; namespace ThunderboltTimeSync { public partial class FormMain : Form { + private static readonly Dictionary LOG_LEVEL_TO_COLOR = new Dictionary() { + { LogLevel.Info, Color.Black }, + { LogLevel.Warning, Color.Yellow }, + { LogLevel.Error, Color.Red } + }; + + private ITimeProvider timeProvider; + public FormMain() { // Check for admin rights // If running as admin: @@ -19,31 +31,29 @@ namespace ThunderboltTimeSync { InitializeComponent(); - ThunderboltSerialPort tbsp = new ThunderboltSerialPort(new SerialPort("COM8")); - - tbsp.PacketReceived += (ThunderboltPacket packet) => { - if (packet.IsPacketValid) { - if (packet.ID == 0x8F && packet.Data.Count == 17 && packet.Data[0] == 0xAB) { - int timeOfWeek = packet.Data[1] << 24 | packet.Data[2] << 16 | packet.Data[3] << 8 | packet.Data[4]; - ushort weekNumber = (ushort) (packet.Data[5] << 8 | packet.Data[6]); - short utcOffset = (short) (packet.Data[7] << 8 | packet.Data[8]); - - // Current epoch for GPS week numbers is the morning of 22/8/1999 - DateTime dateTime = new DateTime(1999, 8, 22, 0, 0, 0); + latestLogMessage.Text = ""; - dateTime = dateTime.AddDays(7 * weekNumber); - dateTime = dateTime.AddSeconds(timeOfWeek); + ThunderboltSerialPort thunderboltSerialPort = new ThunderboltSerialPort(new SerialPort("COM3")); + timeProvider = new ThunderboltTimeProvider(thunderboltSerialPort); - dateTime = dateTime.AddSeconds(-utcOffset); + timeProvider.TimeAvailable += (DateTime dateTime) => { + Invoke(new Action(() => { + labelTimestamps.Text += string.Format("{0} {1} @ {2}\n", dateTime.ToLongDateString(), dateTime.ToLongTimeString(), DateTime.Now.ToLongTimeString()); + })); + }; - labelTimestamps.Invoke(new Action(() => { - labelTimestamps.Text += string.Format("{0} {1}\n", dateTime.ToLongDateString(), dateTime.ToLongTimeString()); - })); - } - } + timeProvider.Log += (string message, LogLevel logLevel) => { + Invoke(new Action(() => { + latestLogMessage.Text = message; + latestLogMessage.ForeColor = LOG_LEVEL_TO_COLOR[logLevel]; + })); }; - tbsp.Open(); + timeProvider.Start(); + } + + private void FormMain_FormClosing(object sender, FormClosingEventArgs e) { + timeProvider.Stop(); } } } diff --git a/ThunderboltTimeSync/FormMain.resx b/ThunderboltTimeSync/FormMain.resx index 1af7de1..422cca5 100644 --- a/ThunderboltTimeSync/FormMain.resx +++ b/ThunderboltTimeSync/FormMain.resx @@ -117,4 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 17, 17 + \ No newline at end of file diff --git a/ThunderboltTimeSync/ThunderboltTimeSync.csproj b/ThunderboltTimeSync/ThunderboltTimeSync.csproj index 8016cf0..fc58a0a 100644 --- a/ThunderboltTimeSync/ThunderboltTimeSync.csproj +++ b/ThunderboltTimeSync/ThunderboltTimeSync.csproj @@ -55,7 +55,9 @@ - + + + FormMain.cs diff --git a/ThunderboltTimeSync/TimeProviders/ITimeProvider.cs b/ThunderboltTimeSync/TimeProviders/ITimeProvider.cs new file mode 100644 index 0000000..58c7fe8 --- /dev/null +++ b/ThunderboltTimeSync/TimeProviders/ITimeProvider.cs @@ -0,0 +1,44 @@ +using System; + +namespace ThunderboltTimeSync.TimeProviders { + /// + /// Called when the time provider has a new time and date available. + /// + /// The current time and date, according to the time provider. + public delegate void TimeAvailableEventHandler(DateTime dateTime); + + /// + /// Called when the time provider wishes to log a message or an error. + /// + /// The message for the log. + /// True if the message constitutes an error, false otherwise. + public delegate void LogEventHandler(string message, LogLevel logLevel); + + public enum LogLevel { + Info, + Warning, + Error + } + + public interface ITimeProvider { + /// + /// Called when the time provider has a new time and date available. + /// + event TimeAvailableEventHandler TimeAvailable; + + /// + /// Called when the time provider wishes to log a message or an error. + /// + event LogEventHandler Log; + + /// + /// Begins providing time information by firing the TimeAvailable event, and log information through the Log event. + /// + void Start(); + + /// + /// Stops providing time information through the TimeAvailable event, and log information through the Log event. + /// + void Stop(); + } +} diff --git a/ThunderboltTimeSync/ThunderboltSerialPort.cs b/ThunderboltTimeSync/TimeProviders/Thunderbolt/ThunderboltSerialPort.cs similarity index 62% rename from ThunderboltTimeSync/ThunderboltSerialPort.cs rename to ThunderboltTimeSync/TimeProviders/Thunderbolt/ThunderboltSerialPort.cs index 00225ed..dc0320c 100644 --- a/ThunderboltTimeSync/ThunderboltSerialPort.cs +++ b/ThunderboltTimeSync/TimeProviders/Thunderbolt/ThunderboltSerialPort.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO.Ports; +using System.Linq; +using System.Threading; -namespace ThunderboltTimeSync { +namespace ThunderboltTimeSync.Devices.Thunderbolt { public class ThunderboltPacket { /// /// The validity of the packet. @@ -45,6 +47,9 @@ namespace ThunderboltTimeSync { private SerialPort serialPort; + private bool running; + private Thread readThread; + /// /// A delegate which is called when a full packet is received over the serial port. /// @@ -58,7 +63,7 @@ namespace ThunderboltTimeSync { /// /// Creates an instance of the ThunderboltSerialPort class, which processes serial data from a Thunderbolt and - /// The serial port passed into the function must not be opened, or have a delegate already attached to the DataReceived event. + /// The serial port passed into the function must not be opened. /// /// The serial port on which to communicate with the Thunderbolt. public ThunderboltSerialPort(SerialPort serialPort) { @@ -66,14 +71,28 @@ namespace ThunderboltTimeSync { inPacket = false; this.serialPort = serialPort; - serialPort.DataReceived += DataReceived; + + readThread = new Thread(ReadSerialPort); } /// - /// Begins processing serial data. + /// Begins processing serial data and firing PacketReceived events. /// public void Open() { + running = true; + serialPort.Open(); + readThread.Start(); + } + + /// + /// Stops processing serial data and firing PacketReceived events. + /// + public void Close() { + running = false; + + readThread.Join(); + serialPort.Close(); } /// @@ -110,6 +129,9 @@ namespace ThunderboltTimeSync { } private void ProcessPacket() { + List byteStrings = packetBuffer.Select(x => string.Format("{0:X2}", x)).ToList(); + Debug.WriteLine(string.Join(" ", byteStrings)); + byte id = packetBuffer[1]; // Grab only the data - not the first [DLE] or the last [DLE][ETX] @@ -131,43 +153,55 @@ namespace ThunderboltTimeSync { // to bring the decoder back into sync with the true packets. It's probably only an issue when the Thunderbolt is initially plugged in, // as that's probably the only time we'd see malformed packets on the serial port, since we could connect in the middle of a packet. // Consider if this is really an issue, and think of a better way of decoding the packets to avoid this. - private void DataReceived(object sender, SerialDataReceivedEventArgs e) { - int possibleCurrentByte; - - while ((possibleCurrentByte = serialPort.ReadByte()) != -1) { - // Once we're sure the byte that was read wasn't -1 (which signifies the end of the read), we're safe to cast to a byte - byte currentByte = (byte) possibleCurrentByte; - - if (inPacket) { - packetBuffer.Add(currentByte); - - // Check buffer length to ensure we've reached a plausible end of packet. - // 5 bytes is [DLE]<1 byte of data>[DLE][ETX] - // Must check if previous character is a [DLE], otherwise an ETX with a malformed and unstuffed [DLE] will cause issues - if (currentByte == CHAR_ETX && packetBuffer.Count >= 5 && packetBuffer[packetBuffer.Count - 2] == CHAR_DLE) { - int numberOfPrecedingDLEs = 0; - - // Count number of DLEs, excluding the first two bytes (initial DLE and id) - for (int i = 2; i < packetBuffer.Count; ++i) { - if (packetBuffer[i] == CHAR_DLE) { - ++numberOfPrecedingDLEs; + private void ReadSerialPort() { + while (running) { + int possibleCurrentByte = serialPort.ReadByte(); + if (possibleCurrentByte != -1) { + // There aren't any packets this long, but during a corrupted or malformed packet the buffer can get quite large before the error + // is fixed somehow. To prevent the almost 20 second delay between time packets that can be caused by some malformed packets, + // just reset everything if the buffer's getting too long. + // We should make sure the user knows, so send a invalid packet to them. + if (packetBuffer.Count >= 128) { + packetBuffer.Clear(); + inPacket = false; + + PacketReceived?.Invoke(new ThunderboltPacket(false, 0, new List(), new List())); + } + + // Once we're sure the byte that was read wasn't -1 (which signifies the end of the read), we're safe to cast to a byte + byte currentByte = (byte) possibleCurrentByte; + + if (inPacket) { + packetBuffer.Add(currentByte); + + // Check buffer length to ensure we've reached a plausible end of packet. + // 5 bytes is [DLE]<1 byte of data>[DLE][ETX] + // Must check if previous character is a [DLE], otherwise an ETX with a malformed and unstuffed [DLE] will cause issues + if (currentByte == CHAR_ETX && packetBuffer.Count >= 5 && packetBuffer[packetBuffer.Count - 2] == CHAR_DLE) { + int numberOfPrecedingDLEs = 0; + + // Count number of DLEs, excluding the first two bytes (initial DLE and id) + for (int i = 2; i < packetBuffer.Count; ++i) { + if (packetBuffer[i] == CHAR_DLE) { + ++numberOfPrecedingDLEs; + } } - } - // Odd number (greater than zero) of DLEs means the ETX does in fact signify the end of the packet - if (numberOfPrecedingDLEs % 2 == 1 && numberOfPrecedingDLEs > 0) { - ProcessPacket(); + // Odd number (greater than zero) of DLEs means the ETX does in fact signify the end of the packet + if (numberOfPrecedingDLEs % 2 == 1 && numberOfPrecedingDLEs > 0) { + ProcessPacket(); - packetBuffer.Clear(); - inPacket = false; + packetBuffer.Clear(); + inPacket = false; + } } - } - } else { - // A DLE received when not currently in a packet signifies the beginning of a packet - if (currentByte == CHAR_DLE) { - packetBuffer.Add(currentByte); + } else { + // A DLE received when not currently in a packet signifies the beginning of a packet + if (currentByte == CHAR_DLE) { + packetBuffer.Add(currentByte); - inPacket = true; + inPacket = true; + } } } } diff --git a/ThunderboltTimeSync/TimeProviders/Thunderbolt/ThunderboltTimeProvider.cs b/ThunderboltTimeSync/TimeProviders/Thunderbolt/ThunderboltTimeProvider.cs new file mode 100644 index 0000000..5bc3096 --- /dev/null +++ b/ThunderboltTimeSync/TimeProviders/Thunderbolt/ThunderboltTimeProvider.cs @@ -0,0 +1,52 @@ +using System; +using ThunderboltTimeSync.Devices.Thunderbolt; + +namespace ThunderboltTimeSync.TimeProviders.Thunderbolt { + class ThunderboltTimeProvider : ITimeProvider { + private ThunderboltSerialPort thunderboltSerialPort; + + public event TimeAvailableEventHandler TimeAvailable; + public event LogEventHandler Log; + + /// + /// Creates an instance of the ThunderboltTimeProvider class, which provides time information through the ITimeProvider interface. + /// The ThunderboltSerialPort instance passed into this function must not be open. + /// + /// The ThunderboltSerialPort instance to use when communicating with the Thunderbolt. + public ThunderboltTimeProvider(ThunderboltSerialPort thunderboltSerialPort) { + this.thunderboltSerialPort = thunderboltSerialPort; + + thunderboltSerialPort.PacketReceived += PacketReceived; + } + + public void Start() { + thunderboltSerialPort.Open(); + } + + public void Stop() { + thunderboltSerialPort.Close(); + } + + private void PacketReceived(ThunderboltPacket packet) { + if (packet.IsPacketValid) { + if (packet.ID == 0x8F && packet.Data.Count == 17 && packet.Data[0] == 0xAB) { + int timeOfWeek = packet.Data[1] << 24 | packet.Data[2] << 16 | packet.Data[3] << 8 | packet.Data[4]; + ushort weekNumber = (ushort) (packet.Data[5] << 8 | packet.Data[6]); + short utcOffset = (short) (packet.Data[7] << 8 | packet.Data[8]); + + // Current epoch for GPS week numbers is the morning of 22/8/1999 + DateTime dateTime = new DateTime(1999, 8, 22, 0, 0, 0); + + dateTime = dateTime.AddDays(7 * weekNumber); + dateTime = dateTime.AddSeconds(timeOfWeek); + + dateTime = dateTime.AddSeconds(-utcOffset); + + TimeAvailable?.Invoke(dateTime); + } + } else { + Log?.Invoke("An invalid packet was received.", LogLevel.Warning); + } + } + } +}