namespace Modbus.IO { using System; using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Message; using Unme.Common; /// /// Modbus transport. /// Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern /// public abstract class ModbusTransport : IDisposable { 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() { } internal ModbusTransport(IStreamResource streamResource) { Debug.Assert(streamResource != null, "Argument streamResource cannot be null."); _streamResource = 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 { return _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 { return _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 { return 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 { return StreamResource.WriteTimeout; } set { StreamResource.WriteTimeout = value; } } /// /// Gets the stream resource. /// internal IStreamResource StreamResource { get { return _streamResource; } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } internal 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 == Modbus.Acknowledge; if (readAgain) { //Debug.WriteLine($"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 != Modbus.SlaveDeviceBusy) { throw; } if (SlaveBusyUsesRetryCount && attempt++ > _retries) { throw; } //Debug.WriteLine($"Received SLAVE_DEVICE_BUSY exception response, waiting {_waitToRetryMilliseconds} milliseconds and resubmitting request."); Sleep(WaitToRetryMilliseconds); } catch (Exception e) { if (e is FormatException || e is NotImplementedException || e is TimeoutException || e is IOException) { //Debug.WriteLine($"{e.GetType().Name}, {(_retries - attempt + 1)} retries remaining - {e}"); if (attempt++ > _retries) { throw; } } else { throw; } } } while (!success); return (T)response; } internal 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; } internal 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 {response.SlaveAddress}, received {request.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) /// internal 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 /// internal 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); internal abstract byte[] ReadRequest(); internal abstract IModbusMessage ReadResponse() where T : IModbusMessage, new(); internal abstract byte[] BuildMessageFrame(IModbusMessage message); internal 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(); } } }