using NLog; using System; using System.Collections.Generic; using System.IO.Ports; using System.Threading; using Xylem.Common.CommonCore.Consts; using Xylem.Common.CommonCore.ThreadWatcher; using Xylem.Common.Hardware.Interfaces.Ports.PortCore; using Xylem.Common.Hardware.Interfaces.Ports.PortCore.EventArguments; using Xylem.Common.Utils.Logging; namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts { /// /// All Ports must use BasePort as an base class /// its support some wrapping event/function handling /// public abstract class BaseSerialPort : IPort, IDisposable { private ILogger _byteDataLogger; internal ILogger AsciiDataLogger; /// public virtual event EventHandler OnRawRecordReceived; /// /// The port is not assigned. /// public const String PortNotAssigned = "NA"; /// public virtual event EventHandler OnRawRecordSendOut; //initially do not signal event private readonly AutoResetEvent _onSyncReceiveThread = new AutoResetEvent(false); private readonly Thread _receiveThread; private readonly CancellationTokenSource _receiveToken = new CancellationTokenSource(); /// /// Delay between bytes if receiving has started in milliseconds /// private const Int32 InterByteReadDelayMs = 50; /// /// internal SerialPort class /// private readonly SerialPort _serialPort; /// /// Activate raw record recording /// public Boolean RecordStreamingRawData = false; //public Boolean RecordStreamingRawData //{ // get => _recordStreamingRawData; // set => _recordStreamingRawData = value; //} /// /// date and time of incoming first date interrupt on serial IO to set the /// PC time-stamp to the record /// private DateTimeOffset _receiveTimeStampPc; /// /// Individual port settings depending on transmit protocol /// protected TransmitPortSettings PortSettingsForTransmitProtocol; /// public String GetPortName() { return _serialPort.PortName; } /// /// store a identification of a port, this contains the Slot, Port, Protocol and Type /// e.g. "Slot:1, Port:COM4, Protocol:Request, Type:IrDA -" /// public String Ident; /// /// ctor for SerialComPort with and full constructed properties /// /// set port as unique /// Class for communication, all parameters for serial communication needs to be set /// Class for store all parameters for serial communication needs to be set came from transmit protocol protected BaseSerialPort(String ident, SerialPort serialPort, TransmitPortSettings setting) { Ident = ident; //communication port settings _serialPort = serialPort; _serialPort.BaudRate = (Int32)setting.BaudRate; PortSettingsForTransmitProtocol = setting; //assign thread to loop _receiveThread = new Thread(ReadingThreadLoop) { Name = $"{Ident} Reading thread" }; AsciiDataLogger = NLogHelper.CreateOrGetMultiLogger(Ident, "", "LedRawData", "TargetLedRawDataBase", "LedRawDataBase"); _byteDataLogger = NLogHelper.CreateOrGetMultiLogger(Ident, "", "UartSerialPort", "COMBase", "BaseSerialPort"); } public void RefreshLogger() { AsciiDataLogger = NLogHelper.CreateOrGetMultiLogger(Ident, "", "LedRawData", "TargetLedRawDataBase", "LedRawDataBase"); _byteDataLogger = NLogHelper.CreateOrGetMultiLogger(Ident, "", "UartSerialPort", "COMBase", "BaseSerialPort"); } /// /// received bytes in buffer of SerialComPort, calls base function /// /// number of bytes in Rx buffer protected Int32 BytesToRead() { try { return _serialPort.BytesToRead; } catch (Exception ex) { _byteDataLogger.Error(ex); return 0; } } /// /// read single byte from Rx buffer of SerialComPort, calls base function /// /// protected Int32 ReadByte() { try { return _serialPort.ReadByte(); } catch (Exception ex) { _byteDataLogger.Error(ex); return 0; } } /// public void PortWrite(Byte[] record) { try { SpecificPortWrite(record); } catch (Exception ex) { throw new SystemException($"{Ident} Communication error while sending data to port", ex); } } /// /// /// flush Rx and Tx buffer of SerialComPort, calls base functions /// public void Clear() { try { if (!_serialPort.IsOpen) { return; } _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); } catch (Exception ex) { _byteDataLogger.Error(ex); } } /// /// Dispose serial port /// /// /// - Dispose procedure changed. /// public void Dispose() { try { //remove registration for receive delegate _serialPort.DataReceived -= PhysicalDataReceived; //Cancel send and receive tokens _receiveToken.Cancel(); //Run thread again to notice CancellationToken has changed _onSyncReceiveThread.Set(); // dispose directly called from here to overcome glitches caused by USB to serial interface _serialPort.Dispose(); } catch (Exception ex) { _byteDataLogger.Error(ex); } } /// public void Open() { try { //set receive interrupt threshold for immediate execution _serialPort.ReceivedBytesThreshold = 1; _serialPort.DataReceived += PhysicalDataReceived; //start reading thread, will be immediately put to waitSleepJoin in ReadingThreadLoop //to avoid side effects with RFID communication ThreadWatcher.Instance.Start(_receiveThread); if (!_serialPort.IsOpen) { _serialPort.Open(); if (!_serialPort.IsOpen) { _byteDataLogger.Error($"{Ident} Port cannot be opened"); throw new ApplicationException($"{Ident} Port cannot be opened"); } _byteDataLogger.Info($"{Ident} Port is opened"); } else { _byteDataLogger.Warn($"{Ident} Port was already opened"); } } catch (Exception ex) { _byteDataLogger.Error($"{Ident} {ex.Message}"); throw new ApplicationException($"{Ident} {ex.Message}"); } } /// /// Getting the base port /// protected SerialPort GetBaseComport() { return _serialPort; } /// /// Fill FIFO with received data /// /// /// - Initial /// /// /// - Time stamp added /// /// /// - Timeout moved from to PhysicalDataReceived to avoid /// of serial port while waiting on first incoming /// data. /// /// /// - Inter byte read delay used instead of response delay!!! /// /// /// public virtual void PhysicalDataReceived(Object sender, SerialDataReceivedEventArgs e) { if (!_receiveToken.IsCancellationRequested) { //remove event delegate to avoid repeated execution during one record _serialPort.DataReceived -= PhysicalDataReceived; //put the actual time stamp to this record being able to assign it correctly //even if the decoding is delayed. This time stamp will be used to do the first //synchronization at start and stop of the measurement. Therefore, the serial buffer //has to be flushed in advance to avoid wrong time stamp to "old" records _receiveTimeStampPc = DateTimeOffset.UtcNow; //start timeout for hanging read communication called inter byte delay _serialPort.ReadTimeout = InterByteReadDelayMs; // _serialPort.ReadTimeout = PortSettingsForTransmitProtocol.ResponseTimeoutMs; //wake up the reading threat from JoinWaitSleep _onSyncReceiveThread.Set(); } } /// /// /// private SyncMarkRecord _syncMarkRecord; /// public void SynchronizeReceiveBuffer(SyncMarkRecord syncMarkRecord) { try { //not in test bench situation only for temp logging if (syncMarkRecord == SyncMarkRecord.FlushBuffer) { FlushBuffer(); return; } //DecodeEveryPackage is ongoing and SkipDecoding is requested = do nothing if (_syncMarkRecord == SyncMarkRecord.DecodeEveryPackage && syncMarkRecord == SyncMarkRecord.SkipDecoding) { return; } //deny intermediate record when a start or end record is requested if (syncMarkRecord == SyncMarkRecord.DecodeIntermediate && (_syncMarkRecord == SyncMarkRecord.SyncEnd || _syncMarkRecord == SyncMarkRecord.SyncStart)) { return; } //being able to detect the first incoming record after a sync is requested, the buffer is going to be flushed //if the measurement will be started or stopped if (syncMarkRecord == SyncMarkRecord.SyncEnd || syncMarkRecord == SyncMarkRecord.SyncStart || syncMarkRecord == SyncMarkRecord.DecodeIntermediate) { FlushBuffer(); } _byteDataLogger.Info($"{Ident} Mark next incoming record as {syncMarkRecord} (was {_syncMarkRecord})"); _syncMarkRecord = syncMarkRecord; } catch (Exception ex) { _byteDataLogger.Error($"{Ident} {ex.Message}"); } } private void FlushBuffer() { //check if flushing is allowed if (null != PortSettingsForTransmitProtocol.ReceiveBufferFlushThreshold) { //flush buffer above threshold to get an accurate actual record with next record coming in if (_serialPort.IsOpen && _serialPort.BytesToRead > PortSettingsForTransmitProtocol.ReceiveBufferFlushThreshold) { _byteDataLogger.Warn($"{Ident} Flushed receive buffer({_serialPort.BytesToRead}Byte)"); _serialPort.DiscardInBuffer(); } } } /// /// Reading thread loop getting data from serial port /// /// /// - Initial /// /// /// - Reading will be executed and repeated until buffer is empty or timeout /// /// /// - Receive byte protocol more dynamically on position of length information in record /// and additional length, /// - decoding of data controlled by dataSyncMarker!= SyncMarkRecord.SkipDecoding to speed up recording /// /// /// - Directly invoked onRawRecord Received event, /// - Doubling of sync byte implemented /// /// /// - Timeout moved from ReadingThreadLoop to to avoid /// of serial port while waiting on first incoming /// data and flush receive buffer at timeout to force task to enter JoinWaitSleep state. /// /// /// - Hide data in logging for e.g. passwords /// /// /// - Raw record recording for missing sync-byte issues. /// /// /// - Try to recover raw record on missing SYNC byte by adding the SYNC upfront and sending /// the record to the decoding thread which will detect if just the SYNC byte had been /// missed, then the CRC will match and the record can be decoded. /// /// /// - Improved recovering of data sets at byte records with preceding SYNC byte: /// - used payload length == 0 to skip decoding, it makes no sense to decode something where /// the payload is empty, /// - wait a certain time to complete incoming data. /// /// /// - Inter byte read delay used instead of response delay. /// /// /// - Flush buffer if received length is 0. /// /// /// - Flush buffer (DiscardInBuffer) removed as some bytes are missing from time to time. /// The IrDA sniffer showed these on the communication line, but they are incompletely received. /// Assuming the DiscardInBuffer may be delayed, so the incoming data will be scrapped. /// /// /// - Dispose procedure changed. /// /// /// - Replaced 'ReadLine' with 'ReadTo' using a string delimiter to support others than LF "\n". /// private void ReadingThreadLoop() { //put the receive thread to JoinWaitSleep to avoid reading and logger output for timeout on //RFID communication, because RFID will handle the physical receive by itself //here the assignment of the _synReadingThreadEvent to the reading thread is being done _onSyncReceiveThread.WaitOne(); try { while (!_receiveToken.IsCancellationRequested) { try { //remind data marker for this Thread execution time slice var dataSyncMarkThisRun = _syncMarkRecord; //read line of ASCII indicated by syncByte == null if (null == PortSettingsForTransmitProtocol.ProtSyncByte) { //exit is timeout from serialPort or received line var rxStringRecord = _serialPort.ReadLine(); if (!string.IsNullOrEmpty(rxStringRecord)) { //decode optional at synchronized data SyncStart, SyncEnd or DecodeIntermediate //todo remove after air problem if (SyncMarkRecord.SkipDecoding != dataSyncMarkThisRun) { OnRawRecordReceived?.Invoke(this, new StringPortDataEventArgs(rxStringRecord, _receiveTimeStampPc, dataSyncMarkThisRun)); } if (RecordStreamingRawData) { AsciiDataLogger.Trace($"{rxStringRecord}"); } } } //byte record with preceding SYNC byte else { //normally decoding is required dataSyncMarkThisRun = SyncMarkRecord.DecodeEveryPackage; //this is the record for decoding var rxByteRecord = new List(); //this is the informational record for logging on undetected SYNC byte var rxRawRecord = new List(); //each byte protocol has to start with a syncByte, this has to be detected first Byte rxByte; do { rxByte = (Byte)_serialPort.ReadByte(); //record raw data stream for output on missing sync-byte to investigate this issue rxRawRecord.Add(rxByte); } while (_serialPort.BytesToRead > 0 && PortSettingsForTransmitProtocol.ProtSyncByte != rxByte); // if syncByte has not been detected skip read loop and wait for next incoming record if (PortSettingsForTransmitProtocol.ProtSyncByte == rxByte) { //save received SYNC byte rxByteRecord.Add(rxByte); //to assemble the length it has to be extracted first from the record at given index, //the payload length index cannot be 0 because at this position is always the SYNC byte, //the overall length includes the SYNC byte! var length = PortSettingsForTransmitProtocol.ProtLengthIndex + 1; //if the protLengthIndex is null the protAddLength equals the entire record length if (null == PortSettingsForTransmitProtocol.ProtLengthIndex) { length = PortSettingsForTransmitProtocol.ProtAddLength; } //read until length index to extract the length from the record //the length index cannot be at position 0, because this is always the syncByte do { rxByte = (Byte)_serialPort.ReadByte(); //read the next byte if doubled sync byte detected and required by transmit protocol settings if (PortSettingsForTransmitProtocol.DoubleSyncByte) { if (rxByte.Equals(PortSettingsForTransmitProtocol.ProtSyncByte) && rxByteRecord[rxByteRecord.Count - 1] .Equals(PortSettingsForTransmitProtocol.ProtSyncByte)) { //skips this byte and read the next one rxByte = (Byte)_serialPort.ReadByte(); } } //add byte to receive result buffer rxByteRecord.Add(rxByte); //capture the length at given index and readjust the record length if (null != PortSettingsForTransmitProtocol.ProtLengthIndex && rxByteRecord.Count - 1 == PortSettingsForTransmitProtocol.ProtLengthIndex) { //if the received length indicates empty payload, then nothing is to decode if (rxByte == 0) { //exit this loop dataSyncMarkThisRun = SyncMarkRecord.SkipDecoding; FlushBuffer(); } else { //build the new length to continue this receive loop length = (UInt16)(PortSettingsForTransmitProtocol.ProtAddLength + rxByte); } } //wait a certain time to let the data stream coming in if (length - rxByteRecord.Count - 1 > _serialPort.BytesToRead) { Thread.Sleep(2); } } while (rxByteRecord.Count < length && dataSyncMarkThisRun != SyncMarkRecord.SkipDecoding); //all data received or skip decoding marked, skip decoding will clear the response timeout //these protocols have always to be decoded because the base is a request protocol OnRawRecordReceived?.Invoke(this, new ListBytePortDataEventArgs(rxByteRecord, _receiveTimeStampPc, dataSyncMarkThisRun)); } else { //output actual byte and recorded raw data stream to investigate missed sync-byte _byteDataLogger.Warn($"{Ident} Response SYNC Byte missing. Raw Byte Received: " + $"{BitConverter.ToString(rxRawRecord.ToArray())}"); } } } catch (ThreadAbortException) { _byteDataLogger.Info($"{Ident} Thread abort exception fired!"); } catch (TimeoutException) { //ATTENTION: This "_serialPort.DiscardInBuffer()" caused a lot of trouble as it flushes a few incoming //bytes and therefore destroys the already started data stream. Here it had been left in to indicate //this critical issue! /*-------------------------------DO NOT ACTIVATE-----------------------------------------------------*/ //flush receive buffer at timeout to force task to enter JoinWaitSleep state in finally //if (_serialPort.IsOpen) //{ // _serialPort.DiscardInBuffer(); //} /*---------------------------------------------------------------------------------------------------*/ _byteDataLogger.Trace($"{Ident} Read timeout({InterByteReadDelayMs}ms)"); //_byteDataLogger.Trace($"{Ident} Read timeout({PortSettingsForTransmitProtocol.ResponseTimeoutMs}ms)"); } catch (Exception ex) { if (_receiveThread.ThreadState == ThreadState.Aborted || _receiveThread.ThreadState == ThreadState.AbortRequested) { _byteDataLogger.Warn(ex, $"{Ident} ThreadState is Aborted or AbortRequested but an " + "error occurred while reading records from serial port"); } else { _byteDataLogger.Error(ex, $"{Ident} Error while reading records from serial port"); } } finally { if (_serialPort.IsOpen && !_receiveToken.IsCancellationRequested) { //it makes no sense to put the thread to sleep if there is something to read if (0 == _serialPort.BytesToRead) { //restore event delegate to activate handle for incoming records _serialPort.DataReceived += PhysicalDataReceived; //put this thread (receive thread) to JainWaitSleep _onSyncReceiveThread.WaitOne(); } } } } //while (!_tokenReadData.IsCancellationRequested) } // over hole reading thread loop catch (ThreadAbortException) { _byteDataLogger.Info($"{Ident} Thread abort exception fired!"); } catch (Exception ex) { _byteDataLogger.Error($"{Ident} {ex.Message}"); } //finally //{ // //var retries = 5; // //while (_serialPort != null && _serialPort.IsOpen && retries-- > 0) // //{ // // _serialPort.Close(); // // if (_serialPort.IsOpen) // // { // // _byteDataLogger.Info($"{Ident} Port closing delay!"); // // Thread.Sleep(500); // // } // //} // //if (_serialPort != null && _serialPort.IsOpen) // // _byteDataLogger.Info($"{Ident} Port unable to close"); // //else // // _byteDataLogger.Info($"{Ident} Port is closed"); //} } /// public Boolean IsOpen() { return _serialPort != null && _serialPort.IsOpen; } /// /// Serial port specific write routine /// /// /// /// - Hide data in logging for e.g. passwords /// protected virtual void SpecificPortWrite(Byte[] txBuffer) { PhysicalWrite(txBuffer); } /// /// Physically sending to the serial port /// /// /// - Initial /// /// /// - Doubling of sync byte in protocol behind sync byte itself implemented /// /// /// - Flush buffer as MOXA sometimes takes two messages and combines these to one! /// As the Genesis needs a separated wake-up message with a following delay before the /// real payload message, this causes a lot of trouble. /// protected void PhysicalWrite(Byte[] txBuffer) { try { //don't double the sync byte itself var txByteList = new List { txBuffer[0] }; for (var byteCtr = 1; byteCtr < txBuffer.Length; byteCtr++) { txByteList.Add(txBuffer[byteCtr]); //write the doubled sync byte again if required by transmit protocol settings if (PortSettingsForTransmitProtocol.DoubleSyncByte) { if (txBuffer[byteCtr].Equals(PortSettingsForTransmitProtocol.ProtSyncByte)) { txByteList.Add(txBuffer[byteCtr]); } } } _serialPort.Write(txByteList.ToArray(), 0, txByteList.Count); // flush the buffer for MOXA, to avoid two subsequent communications assembled to one communication! _serialPort.BaseStream.Flush(); OnRawRecordSendOut?.Invoke(this, new ListBytePortDataEventArgs(txByteList, DateTimeOffset.UtcNow)); } catch (Exception ex) { _byteDataLogger.Error($"{Ident} {ex.Message}"); } } } }