Files
laatzen/Common/Hardware/Interfaces/Ports/SerialPorts/BaseSerialPort.cs
T
Thomas Wiedebusch bac97f8e57 BaseSerialPort:
- 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.

IrdaTransmitProtocol:
- WakeUpMessage marked and debug output
2022-07-15 17:18:12 +02:00

679 lines
28 KiB
C#

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
{
/// <summary>
/// All Ports must use BasePort as an base class
/// its support some wrapping event/function handling
/// </summary>
public abstract class BaseSerialPort : IPort, IDisposable
{
private ILogger _byteDataLogger;
internal ILogger AsciiDataLogger;
/// <inheritdoc />
public virtual event EventHandler<BasePortDataEventArgs> OnRawRecordReceived;
/// <inheritdoc />
public virtual event EventHandler<BasePortDataEventArgs> OnRawRecordSendOut;
//initially do not signal event
private readonly AutoResetEvent _onSyncReceiveThread = new AutoResetEvent(false);
private readonly Thread _receiveThread;
private readonly CancellationTokenSource _receiveToken = new CancellationTokenSource();
/// <summary>
/// Delay between bytes if receiving has started in milliseconds
/// </summary>
private const Int32 InterByteReadDelayMs = 50;
/// <summary>
/// internal SerialPort class
/// </summary>
private readonly SerialPort _serialPort;
/// <summary>
/// Activate raw record recording
/// </summary>
public Boolean RecordStreamingRawData = false;
//public Boolean RecordStreamingRawData
//{
// get => _recordStreamingRawData;
// set => _recordStreamingRawData = value;
//}
/// <summary>
/// date and time of incoming first date interrupt on serial IO to set the
/// PC time-stamp to the record
/// </summary>
private DateTimeOffset _receiveTimeStampPc;
/// <summary>
/// Individual port settings depending on transmit protocol
/// </summary>
protected TransmitPortSettings PortSettingsForTransmitProtocol;
/// <inheritdoc />
public String GetPortName()
{
return _serialPort.PortName;
}
/// <summary>
/// store a identification of a port, this contains the Slot, Port, Protocol and Type
/// e.g. "Slot:1, Port:COM4, Protocol:Request, Type:IrDA -"
/// </summary>
public String Ident;
/// <summary>
/// ctor for SerialComPort with and full constructed <see cref="T:System.IO.Ports.SerialPort" /> properties
/// </summary>
/// <param name="ident">set port as unique</param>
/// <param name="serialPort">Class for communication, all parameters for serial communication needs to be set</param>
/// <param name="setting">Class for store all parameters for serial communication needs to be set came from transmit protocol</param>
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");
}
/// <summary>
/// received bytes in buffer of SerialComPort, calls base function
/// </summary>
/// <returns>number of bytes in Rx buffer</returns>
protected Int32 BytesToRead()
{
try
{
return _serialPort.BytesToRead;
}
catch (Exception ex)
{
_byteDataLogger.Error(ex);
return 0;
}
}
/// <summary>
/// read single byte from Rx buffer of SerialComPort, calls base function
/// </summary>
/// <returns></returns>
protected Int32 ReadByte()
{
try
{
return _serialPort.ReadByte();
}
catch (Exception ex)
{
_byteDataLogger.Error(ex);
return 0;
}
}
/// <inheritdoc />
public void PortWrite(Byte[] record)
{
try
{
SpecificPortWrite(record);
}
catch (Exception ex)
{
throw new SystemException($"{Ident} Communication error while sending data to port", ex);
}
}
/// <inheritdoc />
/// <summary>
/// flush Rx and Tx buffer of SerialComPort, calls base functions
/// </summary>
public void Clear()
{
try
{
if (!_serialPort.IsOpen)
{
return;
}
_serialPort.DiscardInBuffer();
_serialPort.DiscardOutBuffer();
}
catch (Exception ex)
{
_byteDataLogger.Error(ex);
}
}
/// <summary>
/// Dispose serial port
/// </summary>
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();
}
catch (Exception ex)
{
_byteDataLogger.Error(ex);
}
}
/// <inheritdoc />
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}");
}
}
/// <summary>
/// Getting the base port
/// </summary>
protected SerialPort GetBaseComport()
{
return _serialPort;
}
/// <summary>
/// Fill FIFO with received data
/// </summary>
/// <remarks date="2018-Feb-16" author="T.Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Feb-21" author="T.Wiedebusch">
/// - Time stamp added
/// </remarks>
/// <remarks date="2018-Dec-14" author="T.Wiedebusch">
/// - Timeout moved from <see cref="ReadingThreadLoop"/> to PhysicalDataReceived to avoid
/// <see cref="TimeoutException"/> of serial port while waiting on first incoming
/// data.
/// </remarks>
/// <remarks date="2019-Dec-12" author="T.Wiedebusch">
/// - Inter byte read delay used instead of response delay!!!
/// </remarks>
/// <param name="sender"></param>
/// <param name="e"></param>
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. Therefor 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();
}
}
/// <summary>
/// <see cref="SyncMarkRecord" />
/// </summary>
private SyncMarkRecord _syncMarkRecord;
/// <inheritdoc />
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();
}
}
}
/// <summary>
/// Reading thread loop getting data from serial port
/// </summary>
/// <remarks date="2018-Feb-16" author="T.Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Mar-08" author="T.Wiedebusch">
/// - Reading will be executed and repeated until buffer is empty or timeout
/// </remarks>
/// <remarks date="2018-Mar-09" author="T.Wiedebusch">
/// - 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
/// </remarks>
/// <remarks date="2018-Oct-23" author="T.Wiedebusch">
/// - Directly invoked onRawRecord Received event,
/// - Doubling of sync byte implemented
/// </remarks>
/// <remarks date="2018-Dec-14" author="T.Wiedebusch">
/// - Timeout moved from ReadingThreadLoop to <see cref="PhysicalDataReceived"/> to avoid
/// <see cref="TimeoutException"/> of serial port while waiting on first incoming
/// data and flush receive buffer at timeout to force task to enter JoinWaitSleep state.
/// </remarks>
/// <remarks date="2019-Apr-11" author="T.Wiedebusch">
/// - Hide data in logging for e.g. passwords
/// </remarks>
/// <remarks date="2019-Dec-03" author="T.Wiedebusch">
/// - Raw record recording for missing sync-byte issues.
/// </remarks>
/// <remarks date="2019-Dec-04" author="T.Wiedebusch">
/// - 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.
/// </remarks>
/// <remarks date="2019-Dec-05" author="T.Wiedebusch">
/// - 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.
/// </remarks>
/// <remarks date="2019-Dec-12" author="T.Wiedebusch">
/// - Inter byte read delay used instead of response delay.
/// </remarks>
/// <remarks date="2019-Dec-14" author="T.Wiedebusch">
/// - Flush buffer if received length is 0.
/// </remarks>
/// <remarks date="2022-Jul-15" author="T.Wiedebusch">
/// - 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.
/// </remarks>
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 or received line
var rxStringRecord = _serialPort.ReadLine();
if (!string.IsNullOrEmpty(rxStringRecord))
{
//decode optional at synchronized data SyncStart, SyncEnd or DecodeIntermediate
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<Byte>();
//this is the informational record for logging on undetected SYNC byte
var rxRawRecord = new List<Byte>();
//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)
{ throw; }
catch (TimeoutException)
{
//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)
{ }
catch (Exception ex)
{
_byteDataLogger.Error($"{Ident} {ex.Message}");
}
finally
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
_serialPort.Dispose();
_byteDataLogger.Info($"{Ident} Port is closed");
}
}
}
/// <inheritdoc />
public Boolean IsOpen()
{
return _serialPort != null && _serialPort.IsOpen;
}
/// <summary>
/// Serial port specific write routine
/// </summary>
/// <param name="txBuffer"></param>
/// <remarks date="2019-Apr-11" author="T.Wiedebusch">
/// - Hide data in logging for e.g. passwords
/// </remarks>
protected virtual void SpecificPortWrite(Byte[] txBuffer)
{
PhysicalWrite(txBuffer);
}
/// <summary>
/// Physically sending to the serial port
/// </summary>
/// <remarks date="2017" author="R.Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Oct-23" author="T.Wiedebusch">
/// - Doubling of sync byte in protocol behind sync byte itself implemented
/// </remarks>
/// <remarks date="2022-Feb-25" author="T.Wiedebusch, R.Drabesch">
/// - 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.
/// </remarks>
protected void PhysicalWrite(Byte[] txBuffer)
{
try
{
//don't double the sync byte itself
var txByteList = new List<Byte> { 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);
//TODO THW flush before sending NOT after
// 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}");
}
}
#region Prop
#endregion
}
}