@@ -24,6 +24,9 @@ | |||||
/// </summary> | /// </summary> | ||||
private void InitializeComponent() { | private void InitializeComponent() { | ||||
this.labelTimestamps = new System.Windows.Forms.Label(); | 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(); | this.SuspendLayout(); | ||||
// | // | ||||
// labelTimestamps | // labelTimestamps | ||||
@@ -34,21 +37,44 @@ | |||||
this.labelTimestamps.Size = new System.Drawing.Size(486, 358); | this.labelTimestamps.Size = new System.Drawing.Size(486, 358); | ||||
this.labelTimestamps.TabIndex = 0; | 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 | // FormMain | ||||
// | // | ||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); | ||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; | ||||
this.ClientSize = new System.Drawing.Size(486, 358); | this.ClientSize = new System.Drawing.Size(486, 358); | ||||
this.Controls.Add(this.statusStrip); | |||||
this.Controls.Add(this.labelTimestamps); | this.Controls.Add(this.labelTimestamps); | ||||
this.Name = "FormMain"; | this.Name = "FormMain"; | ||||
this.Text = "Thunderbolt Time Sync"; | 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.ResumeLayout(false); | ||||
this.PerformLayout(); | |||||
} | } | ||||
#endregion | #endregion | ||||
private System.Windows.Forms.Label labelTimestamps; | private System.Windows.Forms.Label labelTimestamps; | ||||
private System.Windows.Forms.StatusStrip statusStrip; | |||||
private System.Windows.Forms.ToolStripStatusLabel latestLogMessage; | |||||
} | } | ||||
} | } | ||||
@@ -1,10 +1,22 @@ | |||||
using System; | using System; | ||||
using System.Diagnostics; | |||||
using System.Collections.Generic; | |||||
using System.Drawing; | |||||
using System.IO.Ports; | using System.IO.Ports; | ||||
using System.Windows.Forms; | using System.Windows.Forms; | ||||
using ThunderboltTimeSync.Devices.Thunderbolt; | |||||
using ThunderboltTimeSync.TimeProviders; | |||||
using ThunderboltTimeSync.TimeProviders.Thunderbolt; | |||||
namespace ThunderboltTimeSync { | namespace ThunderboltTimeSync { | ||||
public partial class FormMain : Form { | public partial class FormMain : Form { | ||||
private static readonly Dictionary<LogLevel, Color> LOG_LEVEL_TO_COLOR = new Dictionary<LogLevel, Color>() { | |||||
{ LogLevel.Info, Color.Black }, | |||||
{ LogLevel.Warning, Color.Yellow }, | |||||
{ LogLevel.Error, Color.Red } | |||||
}; | |||||
private ITimeProvider timeProvider; | |||||
public FormMain() { | public FormMain() { | ||||
// Check for admin rights | // Check for admin rights | ||||
// If running as admin: | // If running as admin: | ||||
@@ -19,31 +31,29 @@ namespace ThunderboltTimeSync { | |||||
InitializeComponent(); | 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(); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -117,4 +117,7 @@ | |||||
<resheader name="writer"> | <resheader name="writer"> | ||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||||
</resheader> | </resheader> | ||||
<metadata name="statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | |||||
<value>17, 17</value> | |||||
</metadata> | |||||
</root> | </root> |
@@ -55,7 +55,9 @@ | |||||
<Compile Include="Program.cs" /> | <Compile Include="Program.cs" /> | ||||
<Compile Include="Properties\AssemblyInfo.cs" /> | <Compile Include="Properties\AssemblyInfo.cs" /> | ||||
<Compile Include="SystemTimeUtils.cs" /> | <Compile Include="SystemTimeUtils.cs" /> | ||||
<Compile Include="ThunderboltSerialPort.cs" /> | |||||
<Compile Include="TimeProviders\ITimeProvider.cs" /> | |||||
<Compile Include="TimeProviders\Thunderbolt\ThunderboltSerialPort.cs" /> | |||||
<Compile Include="TimeProviders\Thunderbolt\ThunderboltTimeProvider.cs" /> | |||||
<EmbeddedResource Include="FormMain.resx"> | <EmbeddedResource Include="FormMain.resx"> | ||||
<DependentUpon>FormMain.cs</DependentUpon> | <DependentUpon>FormMain.cs</DependentUpon> | ||||
</EmbeddedResource> | </EmbeddedResource> | ||||
@@ -0,0 +1,44 @@ | |||||
using System; | |||||
namespace ThunderboltTimeSync.TimeProviders { | |||||
/// <summary> | |||||
/// Called when the time provider has a new time and date available. | |||||
/// </summary> | |||||
/// <param name="time">The current time and date, according to the time provider.</param> | |||||
public delegate void TimeAvailableEventHandler(DateTime dateTime); | |||||
/// <summary> | |||||
/// Called when the time provider wishes to log a message or an error. | |||||
/// </summary> | |||||
/// <param name="message">The message for the log.</param> | |||||
/// <param name="isError">True if the message constitutes an error, false otherwise.</param> | |||||
public delegate void LogEventHandler(string message, LogLevel logLevel); | |||||
public enum LogLevel { | |||||
Info, | |||||
Warning, | |||||
Error | |||||
} | |||||
public interface ITimeProvider { | |||||
/// <summary> | |||||
/// Called when the time provider has a new time and date available. | |||||
/// </summary> | |||||
event TimeAvailableEventHandler TimeAvailable; | |||||
/// <summary> | |||||
/// Called when the time provider wishes to log a message or an error. | |||||
/// </summary> | |||||
event LogEventHandler Log; | |||||
/// <summary> | |||||
/// Begins providing time information by firing the TimeAvailable event, and log information through the Log event. | |||||
/// </summary> | |||||
void Start(); | |||||
/// <summary> | |||||
/// Stops providing time information through the TimeAvailable event, and log information through the Log event. | |||||
/// </summary> | |||||
void Stop(); | |||||
} | |||||
} |
@@ -1,8 +1,10 @@ | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.IO.Ports; | using System.IO.Ports; | ||||
using System.Linq; | |||||
using System.Threading; | |||||
namespace ThunderboltTimeSync { | |||||
namespace ThunderboltTimeSync.Devices.Thunderbolt { | |||||
public class ThunderboltPacket { | public class ThunderboltPacket { | ||||
/// <summary> | /// <summary> | ||||
/// The validity of the packet. | /// The validity of the packet. | ||||
@@ -45,6 +47,9 @@ namespace ThunderboltTimeSync { | |||||
private SerialPort serialPort; | private SerialPort serialPort; | ||||
private bool running; | |||||
private Thread readThread; | |||||
/// <summary> | /// <summary> | ||||
/// A delegate which is called when a full packet is received over the serial port. | /// A delegate which is called when a full packet is received over the serial port. | ||||
/// </summary> | /// </summary> | ||||
@@ -58,7 +63,7 @@ namespace ThunderboltTimeSync { | |||||
/// <summary> | /// <summary> | ||||
/// Creates an instance of the ThunderboltSerialPort class, which processes serial data from a Thunderbolt and | /// 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. | |||||
/// </summary> | /// </summary> | ||||
/// <param name="serialPort">The serial port on which to communicate with the Thunderbolt.</param> | /// <param name="serialPort">The serial port on which to communicate with the Thunderbolt.</param> | ||||
public ThunderboltSerialPort(SerialPort serialPort) { | public ThunderboltSerialPort(SerialPort serialPort) { | ||||
@@ -66,14 +71,28 @@ namespace ThunderboltTimeSync { | |||||
inPacket = false; | inPacket = false; | ||||
this.serialPort = serialPort; | this.serialPort = serialPort; | ||||
serialPort.DataReceived += DataReceived; | |||||
readThread = new Thread(ReadSerialPort); | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
/// Begins processing serial data. | |||||
/// Begins processing serial data and firing PacketReceived events. | |||||
/// </summary> | /// </summary> | ||||
public void Open() { | public void Open() { | ||||
running = true; | |||||
serialPort.Open(); | serialPort.Open(); | ||||
readThread.Start(); | |||||
} | |||||
/// <summary> | |||||
/// Stops processing serial data and firing PacketReceived events. | |||||
/// </summary> | |||||
public void Close() { | |||||
running = false; | |||||
readThread.Join(); | |||||
serialPort.Close(); | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -110,6 +129,9 @@ namespace ThunderboltTimeSync { | |||||
} | } | ||||
private void ProcessPacket() { | private void ProcessPacket() { | ||||
List<string> byteStrings = packetBuffer.Select(x => string.Format("{0:X2}", x)).ToList(); | |||||
Debug.WriteLine(string.Join(" ", byteStrings)); | |||||
byte id = packetBuffer[1]; | byte id = packetBuffer[1]; | ||||
// Grab only the data - not the first [DLE]<id> or the last [DLE][ETX] | // Grab only the data - not the first [DLE]<id> 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, | // 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. | // 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. | // 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]<id><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<byte>(), new List<byte>())); | |||||
} | |||||
// 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]<id><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; | |||||
} | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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; | |||||
/// <summary> | |||||
/// 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. | |||||
/// </summary> | |||||
/// <param name="thunderboltSerialPort">The ThunderboltSerialPort instance to use when communicating with the Thunderbolt.</param> | |||||
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); | |||||
} | |||||
} | |||||
} | |||||
} |