using System; using System.Diagnostics; using System.Globalization; using System.IO; using System.Threading; using log4net; using Modbus.Message; namespace Modbus.IO { /// /// Modbus transport. /// Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern /// public abstract class ModbusTransport : IDisposable { private static readonly ILog _logger = LogManager.GetLogger(typeof(ModbusTransport)); private 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; } } /// /// 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 readAgain; bool success = false; do { try { lock (_syncLock) { Write(message); do { readAgain = false; response = ReadResponse(); SlaveExceptionResponse exceptionResponse = response as SlaveExceptionResponse; if (exceptionResponse != null) { // if SlaveExceptionCode == ACKNOWLEDGE we retry reading the response without resubmitting request if (readAgain = exceptionResponse.SlaveExceptionCode == Modbus.Acknowledge) { _logger.InfoFormat("Received ACKNOWLEDGE slave exception response, waiting {0} milliseconds and retrying to read response.", _waitToRetryMilliseconds); Thread.Sleep(WaitToRetryMilliseconds); } else { throw new SlaveException(exceptionResponse); } } } while (readAgain); } ValidateResponse(message, response); success = true; } catch (SlaveException se) { if (se.SlaveExceptionCode != Modbus.SlaveDeviceBusy) throw; _logger.InfoFormat("Received SLAVE_DEVICE_BUSY exception response, waiting {0} milliseconds and resubmitting request.", _waitToRetryMilliseconds); Thread.Sleep(WaitToRetryMilliseconds); } catch (Exception e) { if (e is FormatException || e is NotImplementedException || e is TimeoutException || e is IOException) { _logger.WarnFormat("{0}, {1} retries remaining - {2}", e.GetType().Name, _retries - attempt + 1, 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) throw new IOException(String.Format(CultureInfo.InvariantCulture, "Received response with unexpected Function Code. Expected {0}, received {1}.", request.FunctionCode, response.FunctionCode)); if (request.SlaveAddress != response.SlaveAddress) throw new IOException(String.Format(CultureInfo.InvariantCulture, "Response slave address does not match request. Expected {0}, received {1}.", response.SlaveAddress, request.SlaveAddress)); // message specific validation IModbusRequest modbusRequest = request as IModbusRequest; if (modbusRequest != null) modbusRequest.ValidateResponse(response); OnValidateResponse(request, response); } /// /// 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) { if (_streamResource != null) { _streamResource.Dispose(); _streamResource = null; } } } } }