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;
}
}
}
}
}