using System; using System.Diagnostics; using System.IO; using System.Net.Sockets; using System.Threading.Tasks; using NModbus.Logging; using NModbus.Message; using NModbus.Unme.Common; namespace NModbus.IO { /// /// Modbus transport. /// Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern /// public abstract class ModbusTransport : IModbusTransport { private readonly object _syncLock = new object(); private int _retries = Modbus.DefaultRetries; private int _waitToRetryMilliseconds = Modbus.DefaultWaitToRetryMilliseconds; private IStreamResource _streamResource; /// /// This constructor is called by the NullTransport. /// internal ModbusTransport(IModbusFactory modbusFactory, IModbusLogger logger) { ModbusFactory = modbusFactory; Logger = logger ?? throw new ArgumentNullException(nameof(logger)); } internal ModbusTransport(IStreamResource streamResource, IModbusFactory modbusFactory, IModbusLogger logger) : this(modbusFactory, logger) { _streamResource = streamResource ?? throw new ArgumentNullException(nameof(streamResource)); } /// /// Number of times to retry sending message after encountering a failure such as an IOException, /// TimeoutException, or a corrupt message. /// public int Retries { get => _retries; set => _retries = value; } /// /// If non-zero, this will cause a second reply to be read if the first is behind the sequence number of the /// request by less than this number. For example, set this to 3, and if when sending request 5, response 3 is /// read, we will attempt to re-read responses. /// public uint RetryOnOldResponseThreshold { get; set; } /// /// If set, Slave Busy exception causes retry count to be used. If false, Slave Busy will cause infinite retries /// public bool SlaveBusyUsesRetryCount { get; set; } /// /// Gets or sets the number of milliseconds the tranport will wait before retrying a message after receiving /// an ACKNOWLEGE or SLAVE DEVICE BUSY slave exception response. /// public int WaitToRetryMilliseconds { get => _waitToRetryMilliseconds; set { if (value < 0) { throw new ArgumentException(Resources.WaitRetryGreaterThanZero); } _waitToRetryMilliseconds = value; } } /// /// Gets or sets the number of milliseconds before a timeout occurs when a read operation does not finish. /// public int ReadTimeout { get => StreamResource.ReadTimeout; set => StreamResource.ReadTimeout = value; } /// /// Gets or sets the number of milliseconds before a timeout occurs when a write operation does not finish. /// public int WriteTimeout { get => StreamResource.WriteTimeout; set => StreamResource.WriteTimeout = value; } /// /// Gets the stream resource. /// public IStreamResource StreamResource => _streamResource; protected IModbusFactory ModbusFactory { get; } /// /// Gets the logger for this instance. /// protected IModbusLogger Logger { get; } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public virtual T UnicastMessage(IModbusMessage message) where T : IModbusMessage, new() { IModbusMessage response = null; int attempt = 1; bool success = false; do { try { lock (_syncLock) { Write(message); bool readAgain; do { readAgain = false; response = ReadResponse(); var exceptionResponse = response as SlaveExceptionResponse; if (exceptionResponse != null) { // if SlaveExceptionCode == ACKNOWLEDGE we retry reading the response without resubmitting request readAgain = exceptionResponse.SlaveExceptionCode == SlaveExceptionCodes.Acknowledge; if (readAgain) { Logger.Debug($"Received ACKNOWLEDGE slave exception response, waiting {_waitToRetryMilliseconds} milliseconds and retrying to read response."); Sleep(WaitToRetryMilliseconds); } else { throw new SlaveException(exceptionResponse); } } else if (ShouldRetryResponse(message, response)) { readAgain = true; } } while (readAgain); } ValidateResponse(message, response); success = true; } catch (SlaveException se) { if (se.SlaveExceptionCode != SlaveExceptionCodes.SlaveDeviceBusy) { throw; } if (SlaveBusyUsesRetryCount && attempt++ > _retries) { throw; } Logger.Warning($"Received SLAVE_DEVICE_BUSY exception response, waiting {_waitToRetryMilliseconds} milliseconds and resubmitting request."); Sleep(WaitToRetryMilliseconds); } catch (Exception e) { if ((e is SocketException socketException && socketException.SocketErrorCode != SocketError.TimedOut) || (e.InnerException is SocketException innerSocketException && innerSocketException.SocketErrorCode != SocketError.TimedOut)) { throw; } if (e is FormatException || e is NotImplementedException || e is TimeoutException || e is IOException || e is SocketException) { Logger.Error($"{e.GetType().Name}, {(_retries - attempt + 1)} retries remaining - {e}"); if (attempt++ > _retries) { throw; } Sleep(WaitToRetryMilliseconds); } else { throw; } } } while (!success); return (T)response; } public virtual IModbusMessage CreateResponse(byte[] frame) where T : IModbusMessage, new() { byte functionCode = frame[1]; IModbusMessage response; // check for slave exception response else create message from frame if (functionCode > Modbus.ExceptionOffset) { response = ModbusMessageFactory.CreateModbusMessage(frame); } else { response = ModbusMessageFactory.CreateModbusMessage(frame); } return response; } public void ValidateResponse(IModbusMessage request, IModbusMessage response) { // always check the function code and slave address, regardless of transport protocol if (request.FunctionCode != response.FunctionCode) { string msg = $"Received response with unexpected Function Code. Expected {request.FunctionCode}, received {response.FunctionCode}."; throw new IOException(msg); } if (request.SlaveAddress != response.SlaveAddress) { string msg = $"Response slave address does not match request. Expected {request.SlaveAddress}, received {response.SlaveAddress}."; throw new IOException(msg); } // message specific validation var req = request as IModbusRequest; if (req != null) { req.ValidateResponse(response); } OnValidateResponse(request, response); } /// /// Check whether we need to attempt to read another response before processing it (e.g. response was from previous request) /// public bool ShouldRetryResponse(IModbusMessage request, IModbusMessage response) { // These checks are enforced in ValidateRequest, we don't want to retry for these if (request.FunctionCode != response.FunctionCode) { return false; } if (request.SlaveAddress != response.SlaveAddress) { return false; } return OnShouldRetryResponse(request, response); } /// /// Provide hook to check whether receiving a response should be retried /// public virtual bool OnShouldRetryResponse(IModbusMessage request, IModbusMessage response) { return false; } /// /// Provide hook to do transport level message validation. /// internal abstract void OnValidateResponse(IModbusMessage request, IModbusMessage response); public abstract byte[] ReadRequest(); public abstract IModbusMessage ReadResponse() where T : IModbusMessage, new(); public abstract byte[] BuildMessageFrame(IModbusMessage message); public abstract void Write(IModbusMessage message); /// /// Releases unmanaged and - optionally - managed resources /// /// /// true to release both managed and unmanaged resources; false to release only /// unmanaged resources. /// protected virtual void Dispose(bool disposing) { if (disposing) { DisposableUtility.Dispose(ref _streamResource); } } private static void Sleep(int millisecondsTimeout) { Task.Delay(millisecondsTimeout).Wait(); } } }