ModbusTransport.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Net.Sockets;
  5. using System.Threading.Tasks;
  6. using NModbus.Logging;
  7. using NModbus.Message;
  8. using NModbus.Unme.Common;
  9. namespace NModbus.IO
  10. {
  11. /// <summary>
  12. /// Modbus transport.
  13. /// Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern
  14. /// </summary>
  15. public abstract class ModbusTransport : IModbusTransport
  16. {
  17. private readonly object _syncLock = new object();
  18. private int _retries = Modbus.DefaultRetries;
  19. private int _waitToRetryMilliseconds = Modbus.DefaultWaitToRetryMilliseconds;
  20. private IStreamResource _streamResource;
  21. /// <summary>
  22. /// This constructor is called by the NullTransport.
  23. /// </summary>
  24. internal ModbusTransport(IModbusFactory modbusFactory, IModbusLogger logger)
  25. {
  26. ModbusFactory = modbusFactory;
  27. Logger = logger ?? throw new ArgumentNullException(nameof(logger));
  28. }
  29. internal ModbusTransport(IStreamResource streamResource, IModbusFactory modbusFactory, IModbusLogger logger)
  30. : this(modbusFactory, logger)
  31. {
  32. _streamResource = streamResource ?? throw new ArgumentNullException(nameof(streamResource));
  33. }
  34. /// <summary>
  35. /// Number of times to retry sending message after encountering a failure such as an IOException,
  36. /// TimeoutException, or a corrupt message.
  37. /// </summary>
  38. public int Retries
  39. {
  40. get => _retries;
  41. set => _retries = value;
  42. }
  43. /// <summary>
  44. /// If non-zero, this will cause a second reply to be read if the first is behind the sequence number of the
  45. /// request by less than this number. For example, set this to 3, and if when sending request 5, response 3 is
  46. /// read, we will attempt to re-read responses.
  47. /// </summary>
  48. public uint RetryOnOldResponseThreshold { get; set; }
  49. /// <summary>
  50. /// If set, Slave Busy exception causes retry count to be used. If false, Slave Busy will cause infinite retries
  51. /// </summary>
  52. public bool SlaveBusyUsesRetryCount { get; set; }
  53. /// <summary>
  54. /// Gets or sets the number of milliseconds the tranport will wait before retrying a message after receiving
  55. /// an ACKNOWLEGE or SLAVE DEVICE BUSY slave exception response.
  56. /// </summary>
  57. public int WaitToRetryMilliseconds
  58. {
  59. get => _waitToRetryMilliseconds;
  60. set
  61. {
  62. if (value < 0)
  63. {
  64. throw new ArgumentException(Resources.WaitRetryGreaterThanZero);
  65. }
  66. _waitToRetryMilliseconds = value;
  67. }
  68. }
  69. /// <summary>
  70. /// Gets or sets the number of milliseconds before a timeout occurs when a read operation does not finish.
  71. /// </summary>
  72. public int ReadTimeout
  73. {
  74. get => StreamResource.ReadTimeout;
  75. set => StreamResource.ReadTimeout = value;
  76. }
  77. /// <summary>
  78. /// Gets or sets the number of milliseconds before a timeout occurs when a write operation does not finish.
  79. /// </summary>
  80. public int WriteTimeout
  81. {
  82. get => StreamResource.WriteTimeout;
  83. set => StreamResource.WriteTimeout = value;
  84. }
  85. /// <summary>
  86. /// Gets the stream resource.
  87. /// </summary>
  88. public IStreamResource StreamResource => _streamResource;
  89. protected IModbusFactory ModbusFactory { get; }
  90. /// <summary>
  91. /// Gets the logger for this instance.
  92. /// </summary>
  93. protected IModbusLogger Logger { get; }
  94. /// <summary>
  95. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  96. /// </summary>
  97. public void Dispose()
  98. {
  99. Dispose(true);
  100. GC.SuppressFinalize(this);
  101. }
  102. public virtual T UnicastMessage<T>(IModbusMessage message)
  103. where T : IModbusMessage, new()
  104. {
  105. IModbusMessage response = null;
  106. int attempt = 1;
  107. bool success = false;
  108. do
  109. {
  110. try
  111. {
  112. lock (_syncLock)
  113. {
  114. Write(message);
  115. bool readAgain;
  116. do
  117. {
  118. readAgain = false;
  119. response = ReadResponse<T>();
  120. var exceptionResponse = response as SlaveExceptionResponse;
  121. if (exceptionResponse != null)
  122. {
  123. // if SlaveExceptionCode == ACKNOWLEDGE we retry reading the response without resubmitting request
  124. readAgain = exceptionResponse.SlaveExceptionCode == SlaveExceptionCodes.Acknowledge;
  125. if (readAgain)
  126. {
  127. Logger.Debug($"Received ACKNOWLEDGE slave exception response, waiting {_waitToRetryMilliseconds} milliseconds and retrying to read response.");
  128. Sleep(WaitToRetryMilliseconds);
  129. }
  130. else
  131. {
  132. throw new SlaveException(exceptionResponse);
  133. }
  134. }
  135. else if (ShouldRetryResponse(message, response))
  136. {
  137. readAgain = true;
  138. }
  139. }
  140. while (readAgain);
  141. }
  142. ValidateResponse(message, response);
  143. success = true;
  144. }
  145. catch (SlaveException se)
  146. {
  147. if (se.SlaveExceptionCode != SlaveExceptionCodes.SlaveDeviceBusy)
  148. {
  149. throw;
  150. }
  151. if (SlaveBusyUsesRetryCount && attempt++ > _retries)
  152. {
  153. throw;
  154. }
  155. Logger.Warning($"Received SLAVE_DEVICE_BUSY exception response, waiting {_waitToRetryMilliseconds} milliseconds and resubmitting request.");
  156. Sleep(WaitToRetryMilliseconds);
  157. }
  158. catch (Exception e)
  159. {
  160. if ((e is SocketException socketException && socketException.SocketErrorCode != SocketError.TimedOut)
  161. || (e.InnerException is SocketException innerSocketException && innerSocketException.SocketErrorCode != SocketError.TimedOut))
  162. {
  163. throw;
  164. }
  165. if (e is FormatException ||
  166. e is NotImplementedException ||
  167. e is TimeoutException ||
  168. e is IOException ||
  169. e is SocketException)
  170. {
  171. Logger.Error($"{e.GetType().Name}, {(_retries - attempt + 1)} retries remaining - {e}");
  172. if (attempt++ > _retries)
  173. {
  174. throw;
  175. }
  176. Sleep(WaitToRetryMilliseconds);
  177. }
  178. else
  179. {
  180. throw;
  181. }
  182. }
  183. }
  184. while (!success);
  185. return (T)response;
  186. }
  187. public virtual IModbusMessage CreateResponse<T>(byte[] frame)
  188. where T : IModbusMessage, new()
  189. {
  190. byte functionCode = frame[1];
  191. IModbusMessage response;
  192. // check for slave exception response else create message from frame
  193. if (functionCode > Modbus.ExceptionOffset)
  194. {
  195. response = ModbusMessageFactory.CreateModbusMessage<SlaveExceptionResponse>(frame);
  196. }
  197. else
  198. {
  199. response = ModbusMessageFactory.CreateModbusMessage<T>(frame);
  200. }
  201. return response;
  202. }
  203. public void ValidateResponse(IModbusMessage request, IModbusMessage response)
  204. {
  205. // always check the function code and slave address, regardless of transport protocol
  206. if (request.FunctionCode != response.FunctionCode)
  207. {
  208. string msg = $"Received response with unexpected Function Code. Expected {request.FunctionCode}, received {response.FunctionCode}.";
  209. throw new IOException(msg);
  210. }
  211. if (request.SlaveAddress != response.SlaveAddress)
  212. {
  213. string msg = $"Response slave address does not match request. Expected {request.SlaveAddress}, received {response.SlaveAddress}.";
  214. throw new IOException(msg);
  215. }
  216. // message specific validation
  217. var req = request as IModbusRequest;
  218. if (req != null)
  219. {
  220. req.ValidateResponse(response);
  221. }
  222. OnValidateResponse(request, response);
  223. }
  224. /// <summary>
  225. /// Check whether we need to attempt to read another response before processing it (e.g. response was from previous request)
  226. /// </summary>
  227. public bool ShouldRetryResponse(IModbusMessage request, IModbusMessage response)
  228. {
  229. // These checks are enforced in ValidateRequest, we don't want to retry for these
  230. if (request.FunctionCode != response.FunctionCode)
  231. {
  232. return false;
  233. }
  234. if (request.SlaveAddress != response.SlaveAddress)
  235. {
  236. return false;
  237. }
  238. return OnShouldRetryResponse(request, response);
  239. }
  240. /// <summary>
  241. /// Provide hook to check whether receiving a response should be retried
  242. /// </summary>
  243. public virtual bool OnShouldRetryResponse(IModbusMessage request, IModbusMessage response)
  244. {
  245. return false;
  246. }
  247. /// <summary>
  248. /// Provide hook to do transport level message validation.
  249. /// </summary>
  250. internal abstract void OnValidateResponse(IModbusMessage request, IModbusMessage response);
  251. public abstract byte[] ReadRequest();
  252. public abstract IModbusMessage ReadResponse<T>()
  253. where T : IModbusMessage, new();
  254. public abstract byte[] BuildMessageFrame(IModbusMessage message);
  255. public abstract void Write(IModbusMessage message);
  256. /// <summary>
  257. /// Releases unmanaged and - optionally - managed resources
  258. /// </summary>
  259. /// <param name="disposing">
  260. /// <c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only
  261. /// unmanaged resources.
  262. /// </param>
  263. protected virtual void Dispose(bool disposing)
  264. {
  265. if (disposing)
  266. {
  267. DisposableUtility.Dispose(ref _streamResource);
  268. }
  269. }
  270. private static void Sleep(int millisecondsTimeout)
  271. {
  272. Task.Delay(millisecondsTimeout).Wait();
  273. }
  274. }
  275. }