@@ -24,6 +24,9 @@ | |||
/// </summary> | |||
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; | |||
} | |||
} | |||
@@ -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<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() { | |||
// 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(); | |||
} | |||
} | |||
} |
@@ -117,4 +117,7 @@ | |||
<resheader name="writer"> | |||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | |||
</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> |
@@ -55,7 +55,9 @@ | |||
<Compile Include="Program.cs" /> | |||
<Compile Include="Properties\AssemblyInfo.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"> | |||
<DependentUpon>FormMain.cs</DependentUpon> | |||
</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.Diagnostics; | |||
using System.IO.Ports; | |||
using System.Linq; | |||
using System.Threading; | |||
namespace ThunderboltTimeSync { | |||
namespace ThunderboltTimeSync.Devices.Thunderbolt { | |||
public class ThunderboltPacket { | |||
/// <summary> | |||
/// The validity of the packet. | |||
@@ -45,6 +47,9 @@ namespace ThunderboltTimeSync { | |||
private SerialPort serialPort; | |||
private bool running; | |||
private Thread readThread; | |||
/// <summary> | |||
/// A delegate which is called when a full packet is received over the serial port. | |||
/// </summary> | |||
@@ -58,7 +63,7 @@ namespace ThunderboltTimeSync { | |||
/// <summary> | |||
/// 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> | |||
/// <param name="serialPort">The serial port on which to communicate with the Thunderbolt.</param> | |||
public ThunderboltSerialPort(SerialPort serialPort) { | |||
@@ -66,14 +71,28 @@ namespace ThunderboltTimeSync { | |||
inPacket = false; | |||
this.serialPort = serialPort; | |||
serialPort.DataReceived += DataReceived; | |||
readThread = new Thread(ReadSerialPort); | |||
} | |||
/// <summary> | |||
/// Begins processing serial data. | |||
/// Begins processing serial data and firing PacketReceived events. | |||
/// </summary> | |||
public void Open() { | |||
running = true; | |||
serialPort.Open(); | |||
readThread.Start(); | |||
} | |||
/// <summary> | |||
/// Stops processing serial data and firing PacketReceived events. | |||
/// </summary> | |||
public void Close() { | |||
running = false; | |||
readThread.Join(); | |||
serialPort.Close(); | |||
} | |||
/// <summary> | |||
@@ -110,6 +129,9 @@ namespace ThunderboltTimeSync { | |||
} | |||
private void ProcessPacket() { | |||
List<string> 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]<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, | |||
// 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]<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); | |||
} | |||
} | |||
} | |||
} |