PLC.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net.Sockets;
  7. using System.Runtime.InteropServices;
  8. using S7.Net.Internal;
  9. using S7.Net.Protocol;
  10. using S7.Net.Types;
  11. namespace S7.Net
  12. {
  13. /// <summary>
  14. /// Creates an instance of S7.Net driver
  15. /// </summary>
  16. public partial class Plc : IDisposable
  17. {
  18. /// <summary>
  19. /// The default port for the S7 protocol.
  20. /// </summary>
  21. public const int DefaultPort = 102;
  22. /// <summary>
  23. /// The default timeout (in milliseconds) used for <see cref="P:ReadTimeout"/> and <see cref="P:WriteTimeout"/>.
  24. /// </summary>
  25. public const int DefaultTimeout = 10_000;
  26. private readonly TaskQueue queue = new TaskQueue();
  27. //TCP connection to device
  28. private TcpClient? tcpClient;
  29. private NetworkStream? _stream;
  30. private int readTimeout = DefaultTimeout; // default no timeout
  31. private int writeTimeout = DefaultTimeout; // default no timeout
  32. /// <summary>
  33. /// IP address of the PLC
  34. /// </summary>
  35. public string IP { get; }
  36. /// <summary>
  37. /// PORT Number of the PLC, default is 102
  38. /// </summary>
  39. public int Port { get; }
  40. /// <summary>
  41. /// The TSAP addresses used during the connection request.
  42. /// </summary>
  43. public TsapPair TsapPair { get; set; }
  44. /// <summary>
  45. /// CPU type of the PLC
  46. /// </summary>
  47. public CpuType CPU { get; }
  48. /// <summary>
  49. /// Rack of the PLC
  50. /// </summary>
  51. public Int16 Rack { get; }
  52. /// <summary>
  53. /// Slot of the CPU of the PLC
  54. /// </summary>
  55. public Int16 Slot { get; }
  56. /// <summary>
  57. /// Max PDU size this cpu supports
  58. /// </summary>
  59. public int MaxPDUSize { get; private set; }
  60. /// <summary>Gets or sets the amount of time that a read operation blocks waiting for data from PLC.</summary>
  61. /// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a read operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the read operation does not time out.</returns>
  62. public int ReadTimeout
  63. {
  64. get => readTimeout;
  65. set
  66. {
  67. readTimeout = value;
  68. if (tcpClient != null) tcpClient.ReceiveTimeout = readTimeout;
  69. }
  70. }
  71. /// <summary>Gets or sets the amount of time that a write operation blocks waiting for data to PLC. </summary>
  72. /// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a write operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the write operation does not time out.</returns>
  73. public int WriteTimeout
  74. {
  75. get => writeTimeout;
  76. set
  77. {
  78. writeTimeout = value;
  79. if (tcpClient != null) tcpClient.SendTimeout = writeTimeout;
  80. }
  81. }
  82. /// <summary>
  83. /// Gets a value indicating whether a connection to the PLC has been established.
  84. /// </summary>
  85. /// <remarks>
  86. /// The <see cref="IsConnected"/> property gets the connection state of the Client socket as
  87. /// of the last I/O operation. When it returns <c>false</c>, the Client socket was either
  88. /// never connected, or is no longer connected.
  89. ///
  90. /// <para>
  91. /// Because the <see cref="IsConnected"/> property only reflects the state of the connection
  92. /// as of the most recent operation, you should attempt to send or receive a message to
  93. /// determine the current state. After the message send fails, this property no longer
  94. /// returns <c>true</c>. Note that this behavior is by design. You cannot reliably test the
  95. /// state of the connection because, in the time between the test and a send/receive, the
  96. /// connection could have been lost. Your code should assume the socket is connected, and
  97. /// gracefully handle failed transmissions.
  98. /// </para>
  99. /// </remarks>
  100. public bool IsConnected => tcpClient?.Connected ?? false;
  101. /// <summary>
  102. /// Creates a PLC object with all the parameters needed for connections.
  103. /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
  104. /// You need slot > 0 if you are connecting to external ethernet card (CP).
  105. /// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
  106. /// </summary>
  107. /// <param name="cpu">CpuType of the PLC (select from the enum)</param>
  108. /// <param name="ip">Ip address of the PLC</param>
  109. /// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
  110. /// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
  111. /// If you use an external ethernet card, this must be set accordingly.</param>
  112. public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
  113. : this(cpu, ip, DefaultPort, rack, slot)
  114. {
  115. }
  116. /// <summary>
  117. /// Creates a PLC object with all the parameters needed for connections.
  118. /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
  119. /// You need slot > 0 if you are connecting to external ethernet card (CP).
  120. /// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
  121. /// </summary>
  122. /// <param name="cpu">CpuType of the PLC (select from the enum)</param>
  123. /// <param name="ip">Ip address of the PLC</param>
  124. /// <param name="port">Port number used for the connection, default 102.</param>
  125. /// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
  126. /// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
  127. /// If you use an external ethernet card, this must be set accordingly.</param>
  128. public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
  129. : this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot))
  130. {
  131. if (!Enum.IsDefined(typeof(CpuType), cpu))
  132. throw new ArgumentException(
  133. $"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.",
  134. nameof(cpu));
  135. CPU = cpu;
  136. Rack = rack;
  137. Slot = slot;
  138. }
  139. /// <summary>
  140. /// Creates a PLC object with all the parameters needed for connections.
  141. /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
  142. /// You need slot > 0 if you are connecting to external ethernet card (CP).
  143. /// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
  144. /// </summary>
  145. /// <param name="ip">Ip address of the PLC</param>
  146. /// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
  147. public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair)
  148. {
  149. }
  150. /// <summary>
  151. /// Creates a PLC object with all the parameters needed for connections. Use this constructor
  152. /// if you want to manually override the TSAP addresses used during the connection request.
  153. /// </summary>
  154. /// <param name="ip">Ip address of the PLC</param>
  155. /// <param name="port">Port number used for the connection, default 102.</param>
  156. /// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
  157. public Plc(string ip, int port, TsapPair tsapPair)
  158. {
  159. if (string.IsNullOrEmpty(ip))
  160. throw new ArgumentException("IP address must valid.", nameof(ip));
  161. IP = ip;
  162. Port = port;
  163. MaxPDUSize = 240;
  164. TsapPair = tsapPair;
  165. }
  166. /// <summary>
  167. /// Close connection to PLC
  168. /// </summary>
  169. public void Close()
  170. {
  171. if (tcpClient != null)
  172. {
  173. if (tcpClient.Connected) tcpClient.Close();
  174. tcpClient = null; // Can not reuse TcpClient once connection gets closed.
  175. }
  176. }
  177. private void AssertPduSizeForRead(ICollection<DataItem> dataItems)
  178. {
  179. // send request limit: 19 bytes of header data, 12 bytes of parameter data for each dataItem
  180. var requiredRequestSize = 19 + dataItems.Count * 12;
  181. if (requiredRequestSize > MaxPDUSize) throw new Exception($"Too many vars requested for read. Request size ({requiredRequestSize}) is larger than protocol limit ({MaxPDUSize}).");
  182. // response limit: 14 bytes of header data, 4 bytes of result data for each dataItem and the actual data
  183. var requiredResponseSize = GetDataLength(dataItems) + dataItems.Count * 4 + 14;
  184. if (requiredResponseSize > MaxPDUSize) throw new Exception($"Too much data requested for read. Response size ({requiredResponseSize}) is larger than protocol limit ({MaxPDUSize}).");
  185. }
  186. private void AssertPduSizeForWrite(ICollection<DataItem> dataItems)
  187. {
  188. // 12 bytes of header data, 18 bytes of parameter data for each dataItem
  189. if (dataItems.Count * 18 + 12 > MaxPDUSize) throw new Exception("Too many vars supplied for write");
  190. // 12 bytes of header data, 16 bytes of data for each dataItem and the actual data
  191. if (GetDataLength(dataItems) + dataItems.Count * 16 + 12 > MaxPDUSize)
  192. throw new Exception("Too much data supplied for write");
  193. }
  194. private void ConfigureConnection()
  195. {
  196. if (tcpClient == null)
  197. {
  198. return;
  199. }
  200. tcpClient.ReceiveTimeout = ReadTimeout;
  201. tcpClient.SendTimeout = WriteTimeout;
  202. }
  203. private int GetDataLength(IEnumerable<DataItem> dataItems)
  204. {
  205. // Odd length variables are 0-padded
  206. return dataItems.Select(di => VarTypeToByteLength(di.VarType, di.Count))
  207. .Sum(len => (len & 1) == 1 ? len + 1 : len);
  208. }
  209. private static void AssertReadResponse(byte[] s7Data, int dataLength)
  210. {
  211. var expectedLength = dataLength + 18;
  212. PlcException NotEnoughBytes() =>
  213. new PlcException(ErrorCode.WrongNumberReceivedBytes,
  214. $"Received {s7Data.Length} bytes: '{BitConverter.ToString(s7Data)}', expected {expectedLength} bytes.")
  215. ;
  216. if (s7Data == null)
  217. throw new PlcException(ErrorCode.WrongNumberReceivedBytes, "No s7Data received.");
  218. if (s7Data.Length < 15) throw NotEnoughBytes();
  219. ValidateResponseCode((ReadWriteErrorCode)s7Data[14]);
  220. if (s7Data.Length < expectedLength) throw NotEnoughBytes();
  221. }
  222. internal static void ValidateResponseCode(ReadWriteErrorCode statusCode)
  223. {
  224. switch (statusCode)
  225. {
  226. case ReadWriteErrorCode.ObjectDoesNotExist:
  227. throw new Exception("Received error from PLC: Object does not exist.");
  228. case ReadWriteErrorCode.DataTypeInconsistent:
  229. throw new Exception("Received error from PLC: Data type inconsistent.");
  230. case ReadWriteErrorCode.DataTypeNotSupported:
  231. throw new Exception("Received error from PLC: Data type not supported.");
  232. case ReadWriteErrorCode.AccessingObjectNotAllowed:
  233. throw new Exception("Received error from PLC: Accessing object not allowed.");
  234. case ReadWriteErrorCode.AddressOutOfRange:
  235. throw new Exception("Received error from PLC: Address out of range.");
  236. case ReadWriteErrorCode.HardwareFault:
  237. throw new Exception("Received error from PLC: Hardware fault.");
  238. case ReadWriteErrorCode.Success:
  239. break;
  240. default:
  241. throw new Exception( $"Invalid response from PLC: statusCode={(byte)statusCode}.");
  242. }
  243. }
  244. public int AutoClearCacheInterval
  245. {
  246. get; set;
  247. } = 1;
  248. private System.DateTime LastClearTime = System.DateTime.Now;
  249. private Stream GetStreamIfAvailable()
  250. {
  251. if (_stream == null)
  252. {
  253. throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
  254. }
  255. if (AutoClearCacheInterval > 0 && (System.DateTime.Now - LastClearTime).TotalMinutes >= AutoClearCacheInterval)
  256. {
  257. ClearCache();
  258. LastClearTime = System.DateTime.Now;
  259. }
  260. return _stream;
  261. }
  262. private void ClearCache()
  263. {
  264. GC.Collect();
  265. GC.WaitForPendingFinalizers();
  266. if (Environment.OSVersion.Platform == PlatformID.Win32NT)
  267. {
  268. SetProcessWorkingSetSize(Process.GetCurrentProcess().Id, -1, -1);
  269. }
  270. }
  271. [DllImport("kernel32.dll")]
  272. private static extern int SetProcessWorkingSetSize(int process, int minSize, int maxSize);
  273. #region IDisposable Support
  274. private bool disposedValue = false; // To detect redundant calls
  275. /// <summary>
  276. /// Dispose Plc Object
  277. /// </summary>
  278. /// <param name="disposing"></param>
  279. protected virtual void Dispose(bool disposing)
  280. {
  281. if (!disposedValue)
  282. {
  283. if (disposing)
  284. {
  285. Close();
  286. }
  287. // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
  288. // TODO: set large fields to null.
  289. disposedValue = true;
  290. }
  291. }
  292. // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
  293. // ~Plc() {
  294. // // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
  295. // Dispose(false);
  296. // }
  297. // This code added to correctly implement the disposable pattern.
  298. void IDisposable.Dispose()
  299. {
  300. // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
  301. Dispose(true);
  302. // TODO: uncomment the following line if the finalizer is overridden above.
  303. // GC.SuppressFinalize(this);
  304. }
  305. #endregion
  306. }
  307. }