PlcAsynchronous.cs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. using S7.Net.Types;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net.Sockets;
  7. using System.Threading.Tasks;
  8. using S7.Net.Protocol;
  9. using System.Threading;
  10. using S7.Net.Protocol.S7;
  11. namespace S7.Net
  12. {
  13. /// <summary>
  14. /// Creates an instance of S7.Net driver
  15. /// </summary>
  16. public partial class Plc
  17. {
  18. /// <summary>
  19. /// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup.
  20. /// </summary>
  21. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  22. /// Please note that the cancellation will not affect opening the socket in any way and only affects data transfers for configuring the connection after the socket connection is successfully established.
  23. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  24. /// <returns>A task that represents the asynchronous open operation.</returns>
  25. public async Task OpenAsync(CancellationToken cancellationToken = default)
  26. {
  27. var stream = await ConnectAsync(cancellationToken).ConfigureAwait(false);
  28. try
  29. {
  30. await queue.Enqueue(async () =>
  31. {
  32. cancellationToken.ThrowIfCancellationRequested();
  33. await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
  34. _stream = stream;
  35. return default(object);
  36. }).ConfigureAwait(false);
  37. }
  38. catch (Exception)
  39. {
  40. stream.Dispose();
  41. throw;
  42. }
  43. }
  44. private async Task<NetworkStream> ConnectAsync(CancellationToken cancellationToken)
  45. {
  46. tcpClient = new TcpClient();
  47. ConfigureConnection();
  48. #if NET5_0_OR_GREATER
  49. await tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false);
  50. #else
  51. await tcpClient.ConnectAsync(IP, Port).ConfigureAwait(false);
  52. #endif
  53. return tcpClient.GetStream();
  54. }
  55. private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken)
  56. {
  57. await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
  58. await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
  59. }
  60. private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
  61. {
  62. var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair);
  63. var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
  64. if (response.PDUType != COTP.PduType.ConnectionConfirmed)
  65. {
  66. throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d);
  67. }
  68. }
  69. private async Task SetupConnection(Stream stream, CancellationToken cancellationToken)
  70. {
  71. var setupData = GetS7ConnectionSetup();
  72. var s7data = await NoLockRequestTsduAsync(stream, setupData, 0, setupData.Length, cancellationToken)
  73. .ConfigureAwait(false);
  74. if (s7data.Length < 2)
  75. throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
  76. //Check for S7 Ack Data
  77. if (s7data[1] != 0x03)
  78. throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
  79. if (s7data.Length < 20)
  80. throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
  81. // TODO: check if this should not rather be UInt16.
  82. MaxPDUSize = s7data[18] * 256 + s7data[19];
  83. }
  84. /// <summary>
  85. /// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
  86. /// If the read was not successful, check LastErrorCode or LastErrorString.
  87. /// </summary>
  88. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  89. /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  90. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  91. /// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
  92. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  93. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  94. /// <returns>Returns the bytes in an array</returns>
  95. public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default)
  96. {
  97. var resultBytes = new byte[count];
  98. await ReadBytesAsync(resultBytes, dataType, db, startByteAdr, cancellationToken).ConfigureAwait(false);
  99. return resultBytes;
  100. }
  101. /// <summary>
  102. /// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
  103. /// If the read was not successful, check LastErrorCode or LastErrorString.
  104. /// </summary>
  105. /// <param name="buffer">Buffer to receive the read bytes. The <see cref="Memory{T}.Length"/> determines the number of bytes to read.</param>
  106. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  107. /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  108. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  109. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  110. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  111. /// <returns>Returns the bytes in an array</returns>
  112. public async Task ReadBytesAsync(Memory<byte> buffer, DataType dataType, int db, int startByteAdr, CancellationToken cancellationToken = default)
  113. {
  114. int index = 0;
  115. while (buffer.Length > 0)
  116. {
  117. //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
  118. var maxToRead = Math.Min(buffer.Length, MaxPDUSize - 18);
  119. await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead), cancellationToken).ConfigureAwait(false);
  120. buffer = buffer.Slice(maxToRead);
  121. index += maxToRead;
  122. }
  123. }
  124. /// <summary>
  125. /// Read and decode a certain number of bytes of the "VarType" provided.
  126. /// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
  127. /// If the read was not successful, check LastErrorCode or LastErrorString.
  128. /// </summary>
  129. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  130. /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  131. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  132. /// <param name="varType">Type of the variable/s that you are reading</param>
  133. /// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
  134. /// <param name="varCount"></param>
  135. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  136. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  137. public async Task<object?> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0, CancellationToken cancellationToken = default)
  138. {
  139. int cntBytes = VarTypeToByteLength(varType, varCount);
  140. byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes, cancellationToken).ConfigureAwait(false);
  141. return ParseBytes(varType, bytes, varCount, bitAdr);
  142. }
  143. /// <summary>
  144. /// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
  145. /// If the read was not successful, check LastErrorCode or LastErrorString.
  146. /// </summary>
  147. /// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
  148. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  149. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  150. /// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
  151. public async Task<object?> ReadAsync(string variable, CancellationToken cancellationToken = default)
  152. {
  153. var adr = new PLCAddress(variable);
  154. return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken).ConfigureAwait(false);
  155. }
  156. /// <summary>
  157. /// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct.
  158. /// </summary>
  159. /// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
  160. /// <param name="db">Address of the DB.</param>
  161. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  162. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  163. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  164. /// <returns>Returns a struct that must be cast.</returns>
  165. public async Task<object?> ReadStructAsync(Type structType, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
  166. {
  167. int numBytes = Types.Struct.GetStructSize(structType);
  168. // now read the package
  169. var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
  170. // and decode it
  171. return Types.Struct.FromBytes(structType, resultBytes);
  172. }
  173. /// <summary>
  174. /// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read.
  175. /// </summary>
  176. /// <typeparam name="T">The struct type</typeparam>
  177. /// <param name="db">Address of the DB.</param>
  178. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  179. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  180. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  181. /// <returns>Returns a nulable struct. If nothing was read null will be returned.</returns>
  182. public async Task<T?> ReadStructAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : struct
  183. {
  184. return await ReadStructAsync(typeof(T), db, startByteAdr, cancellationToken).ConfigureAwait(false) as T?;
  185. }
  186. /// <summary>
  187. /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
  188. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
  189. /// </summary>
  190. /// <param name="sourceClass">Instance of the class that will store the values</param>
  191. /// <param name="db">Index of the DB; es.: 1 is for DB1</param>
  192. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  193. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  194. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  195. /// <returns>The number of read bytes</returns>
  196. public async Task<Tuple<int, object>> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
  197. {
  198. int numBytes = (int)Class.GetClassSize(sourceClass);
  199. if (numBytes <= 0)
  200. {
  201. throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
  202. }
  203. // now read the package
  204. var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
  205. // and decode it
  206. Class.FromBytes(sourceClass, resultBytes);
  207. return new Tuple<int, object>(resultBytes.Length, sourceClass);
  208. }
  209. /// <summary>
  210. /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
  211. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
  212. /// type, the class needs a default constructor.
  213. /// </summary>
  214. /// <typeparam name="T">The class that will be instantiated. Requires a default constructor</typeparam>
  215. /// <param name="db">Index of the DB; es.: 1 is for DB1</param>
  216. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  217. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  218. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  219. /// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
  220. public async Task<T?> ReadClassAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
  221. {
  222. return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr, cancellationToken).ConfigureAwait(false);
  223. }
  224. /// <summary>
  225. /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
  226. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
  227. /// </summary>
  228. /// <typeparam name="T">The class that will be instantiated</typeparam>
  229. /// <param name="classFactory">Function to instantiate the class</param>
  230. /// <param name="db">Index of the DB; es.: 1 is for DB1</param>
  231. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  232. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  233. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  234. /// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
  235. public async Task<T?> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
  236. {
  237. var instance = classFactory();
  238. var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken).ConfigureAwait(false);
  239. int readBytes = res.Item1;
  240. if (readBytes <= 0)
  241. {
  242. return null;
  243. }
  244. return (T)res.Item2;
  245. }
  246. /// <summary>
  247. /// Reads multiple vars in a single request.
  248. /// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
  249. /// Values are stored in the property "Value" of the dataItem and are already converted.
  250. /// If you don't want the conversion, just create a dataItem of bytes.
  251. /// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
  252. /// </summary>
  253. /// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
  254. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  255. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  256. public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems, CancellationToken cancellationToken = default)
  257. {
  258. //Snap7 seems to choke on PDU sizes above 256 even if snap7
  259. //replies with bigger PDU size in connection setup.
  260. AssertPduSizeForRead(dataItems);
  261. try
  262. {
  263. var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList());
  264. var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
  265. ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
  266. ParseDataIntoDataItems(s7data, dataItems);
  267. }
  268. catch (SocketException socketException)
  269. {
  270. throw new PlcException(ErrorCode.ReadData, socketException);
  271. }
  272. catch (OperationCanceledException)
  273. {
  274. throw;
  275. }
  276. catch (Exception exc)
  277. {
  278. throw new PlcException(ErrorCode.ReadData, exc);
  279. }
  280. return dataItems;
  281. }
  282. /// <summary>
  283. /// Read the PLC clock value.
  284. /// </summary>
  285. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  286. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  287. /// <returns>A task that represents the asynchronous operation, with it's result set to the current PLC time on completion.</returns>
  288. public async Task<System.DateTime> ReadClockAsync(CancellationToken cancellationToken = default)
  289. {
  290. var request = BuildClockReadRequest();
  291. var response = await RequestTsduAsync(request, cancellationToken);
  292. return ParseClockReadResponse(response);
  293. }
  294. /// <summary>
  295. /// Write the PLC clock value.
  296. /// </summary>
  297. /// <param name="value">The date and time to set the PLC clock to</param>
  298. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  299. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  300. /// <returns>A task that represents the asynchronous operation.</returns>
  301. public async Task WriteClockAsync(System.DateTime value, CancellationToken cancellationToken = default)
  302. {
  303. var request = BuildClockWriteRequest(value);
  304. var response = await RequestTsduAsync(request, cancellationToken);
  305. ParseClockWriteResponse(response);
  306. }
  307. /// <summary>
  308. /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
  309. /// </summary>
  310. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  311. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  312. /// <returns>A task that represents the asynchronous operation, with it's result set to the current PLC status on completion.</returns>
  313. public async Task<byte> ReadStatusAsync(CancellationToken cancellationToken = default)
  314. {
  315. var dataToSend = BuildSzlReadRequestPackage(0x0424, 0);
  316. var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
  317. return (byte) (s7data[37] & 0x0f);
  318. }
  319. /// <summary>
  320. /// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
  321. /// If the write was not successful, check LastErrorCode or LastErrorString.
  322. /// </summary>
  323. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  324. /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  325. /// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
  326. /// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
  327. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  328. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  329. /// <returns>A task that represents the asynchronous write operation.</returns>
  330. public Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken = default)
  331. {
  332. return WriteBytesAsync(dataType, db, startByteAdr, value.AsMemory(), cancellationToken);
  333. }
  334. /// <summary>
  335. /// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
  336. /// If the write was not successful, check LastErrorCode or LastErrorString.
  337. /// </summary>
  338. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  339. /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  340. /// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
  341. /// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
  342. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  343. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  344. /// <returns>A task that represents the asynchronous write operation.</returns>
  345. public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken = default)
  346. {
  347. int localIndex = 0;
  348. while (value.Length > 0)
  349. {
  350. var maxToWrite = (int)Math.Min(value.Length, MaxPDUSize - 35);
  351. await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite), cancellationToken).ConfigureAwait(false);
  352. value = value.Slice(maxToWrite);
  353. localIndex += maxToWrite;
  354. }
  355. }
  356. /// <summary>
  357. /// Write a single bit from a DB with the specified index.
  358. /// </summary>
  359. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  360. /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  361. /// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
  362. /// <param name="bitAdr">The address of the bit. (0-7)</param>
  363. /// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
  364. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  365. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  366. /// <returns>A task that represents the asynchronous write operation.</returns>
  367. public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value, CancellationToken cancellationToken = default)
  368. {
  369. if (bitAdr < 0 || bitAdr > 7)
  370. throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr));
  371. await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value, cancellationToken).ConfigureAwait(false);
  372. }
  373. /// <summary>
  374. /// Write a single bit from a DB with the specified index.
  375. /// </summary>
  376. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  377. /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  378. /// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
  379. /// <param name="bitAdr">The address of the bit. (0-7)</param>
  380. /// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
  381. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  382. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  383. /// <returns>A task that represents the asynchronous write operation.</returns>
  384. public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value, CancellationToken cancellationToken = default)
  385. {
  386. if (value < 0 || value > 1)
  387. throw new ArgumentException("Value must be 0 or 1", nameof(value));
  388. await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1, cancellationToken).ConfigureAwait(false);
  389. }
  390. /// <summary>
  391. /// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type.
  392. /// You must specify the memory area type, memory are address, byte start address and bytes count.
  393. /// If the read was not successful, check LastErrorCode or LastErrorString.
  394. /// </summary>
  395. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  396. /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  397. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  398. /// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
  399. /// <param name="bitAdr">The address of the bit. (0-7)</param>
  400. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  401. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  402. /// <returns>A task that represents the asynchronous write operation.</returns>
  403. public async Task WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1, CancellationToken cancellationToken = default)
  404. {
  405. if (bitAdr != -1)
  406. {
  407. //Must be writing a bit value as bitAdr is specified
  408. if (value is bool boolean)
  409. {
  410. await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken).ConfigureAwait(false);
  411. }
  412. else if (value is int intValue)
  413. {
  414. if (intValue < 0 || intValue > 7)
  415. throw new ArgumentOutOfRangeException(
  416. string.Format(
  417. "Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid",
  418. bitAdr), nameof(bitAdr));
  419. await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1, cancellationToken).ConfigureAwait(false);
  420. }
  421. else throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
  422. }
  423. else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value), cancellationToken).ConfigureAwait(false);
  424. }
  425. /// <summary>
  426. /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
  427. /// </summary>
  428. /// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
  429. /// <param name="value">Value to be written to the PLC</param>
  430. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  431. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  432. /// <returns>A task that represents the asynchronous write operation.</returns>
  433. public async Task WriteAsync(string variable, object value, CancellationToken cancellationToken = default)
  434. {
  435. var adr = new PLCAddress(variable);
  436. await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken).ConfigureAwait(false);
  437. }
  438. /// <summary>
  439. /// Writes a C# struct to a DB in the PLC
  440. /// </summary>
  441. /// <param name="structValue">The struct to be written</param>
  442. /// <param name="db">Db address</param>
  443. /// <param name="startByteAdr">Start bytes on the PLC</param>
  444. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  445. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  446. /// <returns>A task that represents the asynchronous write operation.</returns>
  447. public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
  448. {
  449. var bytes = Struct.ToBytes(structValue).ToList();
  450. await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken).ConfigureAwait(false);
  451. }
  452. /// <summary>
  453. /// Writes a C# class to a DB in the PLC
  454. /// </summary>
  455. /// <param name="classValue">The class to be written</param>
  456. /// <param name="db">Db address</param>
  457. /// <param name="startByteAdr">Start bytes on the PLC</param>
  458. /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
  459. /// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
  460. /// <returns>A task that represents the asynchronous write operation.</returns>
  461. public async Task WriteClassAsync(object classValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
  462. {
  463. byte[] bytes = new byte[(int)Class.GetClassSize(classValue)];
  464. Types.Class.ToBytes(classValue, bytes);
  465. await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false);
  466. }
  467. private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, Memory<byte> buffer, CancellationToken cancellationToken)
  468. {
  469. var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, buffer.Length) });
  470. var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
  471. AssertReadResponse(s7data, buffer.Length);
  472. s7data.AsSpan(18, buffer.Length).CopyTo(buffer.Span);
  473. }
  474. /// <summary>
  475. /// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
  476. /// or when the PLC reports errors for item(s) written.
  477. /// </summary>
  478. /// <param name="dataItems">The DataItem(s) to write to the PLC.</param>
  479. /// <returns>Task that completes when response from PLC is parsed.</returns>
  480. public async Task WriteAsync(params DataItem[] dataItems)
  481. {
  482. AssertPduSizeForWrite(dataItems);
  483. var message = new ByteArray();
  484. var length = S7WriteMultiple.CreateRequest(message, dataItems);
  485. var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false);
  486. S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
  487. }
  488. /// <summary>
  489. /// Writes up to 200 bytes to the PLC. You must specify the memory area type, memory are address, byte start address and bytes count.
  490. /// </summary>
  491. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  492. /// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  493. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  494. /// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
  495. /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
  496. /// <returns>A task that represents the asynchronous write operation.</returns>
  497. private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken)
  498. {
  499. try
  500. {
  501. var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value.Span);
  502. var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
  503. ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
  504. }
  505. catch (OperationCanceledException)
  506. {
  507. throw;
  508. }
  509. catch (Exception exc)
  510. {
  511. throw new PlcException(ErrorCode.WriteData, exc);
  512. }
  513. }
  514. private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
  515. {
  516. try
  517. {
  518. var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
  519. var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
  520. ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
  521. }
  522. catch (OperationCanceledException)
  523. {
  524. throw;
  525. }
  526. catch (Exception exc)
  527. {
  528. throw new PlcException(ErrorCode.WriteData, exc);
  529. }
  530. }
  531. private Task<byte[]> RequestTsduAsync(byte[] requestData, CancellationToken cancellationToken = default) =>
  532. RequestTsduAsync(requestData, 0, requestData.Length, cancellationToken);
  533. private Task<byte[]> RequestTsduAsync(byte[] requestData, int offset, int length, CancellationToken cancellationToken = default)
  534. {
  535. var stream = GetStreamIfAvailable();
  536. return queue.Enqueue(() =>
  537. NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
  538. }
  539. private async Task<COTP.TPDU> NoLockRequestTpduAsync(Stream stream, byte[] requestData,
  540. CancellationToken cancellationToken = default)
  541. {
  542. cancellationToken.ThrowIfCancellationRequested();
  543. try
  544. {
  545. using var closeOnCancellation = cancellationToken.Register(Close);
  546. await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false);
  547. return await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
  548. }
  549. catch (Exception exc)
  550. {
  551. if (exc is TPDUInvalidException || exc is TPKTInvalidException)
  552. {
  553. Close();
  554. }
  555. throw;
  556. }
  557. }
  558. private async Task<byte[]> NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
  559. CancellationToken cancellationToken = default)
  560. {
  561. cancellationToken.ThrowIfCancellationRequested();
  562. try
  563. {
  564. using var closeOnCancellation = cancellationToken.Register(Close);
  565. await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false);
  566. return await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
  567. }
  568. catch (Exception exc)
  569. {
  570. if (exc is TPDUInvalidException || exc is TPKTInvalidException)
  571. {
  572. Close();
  573. }
  574. throw;
  575. }
  576. }
  577. }
  578. }