using System.Collections.Generic; using System.Diagnostics; using System.IO.Ports; namespace ThunderboltTimeSync { public class ThunderboltPacket { /// /// The validity of the packet. /// If false, the values of ID, Data and RawData must not be used. /// If true, the packet may still contain invalid values or be of incorrect length. /// public bool IsPacketValid { get; } /// /// The ID of the packet. /// public byte ID { get; } /// /// The data contained within the packet. /// public List Data { get; } /// /// The raw data of the packet. /// Contains the initial [DLE] byte, and the terminating [DLE][ETX] bytes. [DLE] values within the data are still stuffed. /// public List RawData { get; } public ThunderboltPacket(bool isPacketValid, byte id, List data, List rawData) { IsPacketValid = isPacketValid; ID = id; Data = data; RawData = rawData; } } public class ThunderboltSerialPort { private static readonly byte CHAR_DLE = 0x10; private static readonly byte CHAR_ETX = 0x03; private List packetBuffer; private bool inPacket; private SerialPort serialPort; /// /// A delegate which is called when a full packet is received over the serial port. /// /// The packet which was received. public delegate void PacketReceivedEventHandler(ThunderboltPacket packet); /// /// An event which is called when a full packet is received over the serial port. /// public event PacketReceivedEventHandler PacketReceived; /// /// 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 on which to communicate with the Thunderbolt. public ThunderboltSerialPort(SerialPort serialPort) { packetBuffer = new List(); inPacket = false; this.serialPort = serialPort; serialPort.DataReceived += DataReceived; } /// /// Begins processing serial data. /// public void Open() { serialPort.Open(); } /// /// Converts a stuffed byte list (one where all 0x10 bytes are replaced with 0x10 0x10) into an unstuffed byte list. /// /// A reference to the list containing the data to be unstuffed. /// True if the unstuffing was successful, false otherwise. private bool Unstuff(ref List data) { List newData = new List(); bool inStuffedDLE = false; foreach (byte b in data) { if (b == CHAR_DLE) { if (!inStuffedDLE) { newData.Add(b); inStuffedDLE = true; } else { // If we see a DLE after we've already seen one (inStuffedDLE == true), we don't need to add the byte to the list because we already did when the first byte was encountered // Just set the flag to false so that if this stuffed DLE is immediately followed by another, it will be correctly parsed inStuffedDLE = false; } } else { if (inStuffedDLE) { return false; } newData.Add(b); } } data = newData; return true; } private void ProcessPacket() { byte id = packetBuffer[1]; // Grab only the data - not the first [DLE] or the last [DLE][ETX] List data = packetBuffer.GetRange(2, packetBuffer.Count - 4); bool isPacketValid = Unstuff(ref data); ThunderboltPacket packet; if (isPacketValid) { packet = new ThunderboltPacket(isPacketValid, id, data, packetBuffer); } else { packet = new ThunderboltPacket(isPacketValid, 0, new List(), new List()); } PacketReceived?.Invoke(packet); } // TODO: Improve this? Currently, if a malformed packet comes in it can take many more packets (totalling up to almost 5 to 10 seconds of time) // 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; } } // 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; } } } 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; } } } } } }