PlcSynchronous.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. using S7.Net.Types;
  2. using System;
  3. using System.IO;
  4. using System.Collections.Generic;
  5. using S7.Net.Protocol;
  6. using S7.Net.Helper;
  7. using System.Runtime.CompilerServices;
  8. using System.Linq;
  9. //Implement synchronous methods here
  10. namespace S7.Net
  11. {
  12. public partial class Plc
  13. {
  14. /// <summary>
  15. /// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup.
  16. /// </summary>
  17. public void Open()
  18. {
  19. try
  20. {
  21. OpenAsync().GetAwaiter().GetResult();
  22. }
  23. catch (Exception exc)
  24. {
  25. throw new PlcException(ErrorCode.ConnectionError,
  26. $"Couldn't establish the connection to {IP}.\nMessage: {exc.Message}", exc);
  27. }
  28. }
  29. /// <summary>
  30. /// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
  31. /// If the read was not successful, check LastErrorCode or LastErrorString.
  32. /// </summary>
  33. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  34. /// <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>
  35. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  36. /// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
  37. /// <returns>Returns the bytes in an array</returns>
  38. public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
  39. {
  40. var result = new byte[count];
  41. ReadBytes(result, dataType, db, startByteAdr);
  42. return result;
  43. }
  44. /// <summary>
  45. /// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
  46. /// If the read was not successful, check LastErrorCode or LastErrorString.
  47. /// </summary>
  48. /// <param name="buffer">Buffer to receive the read bytes. The <see cref="Span{T}.Length"/> determines the number of bytes to read.</param>
  49. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  50. /// <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>
  51. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  52. /// <returns>Returns the bytes in an array</returns>
  53. public void ReadBytes(Span<byte> buffer, DataType dataType, int db, int startByteAdr)
  54. {
  55. int index = 0;
  56. while (buffer.Length > 0)
  57. {
  58. //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
  59. var maxToRead = Math.Min(buffer.Length, MaxPDUSize - 18);
  60. ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead));
  61. buffer = buffer.Slice(maxToRead);
  62. index += maxToRead;
  63. }
  64. }
  65. /// <summary>
  66. /// Read and decode a certain number of bytes of the "VarType" provided.
  67. /// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
  68. /// If the read was not successful, check LastErrorCode or LastErrorString.
  69. /// </summary>
  70. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  71. /// <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>
  72. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  73. /// <param name="varType">Type of the variable/s that you are reading</param>
  74. /// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
  75. /// <param name="varCount"></param>
  76. public object? Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
  77. {
  78. int cntBytes = VarTypeToByteLength(varType, varCount);
  79. byte[] bytes = ReadBytes(dataType, db, startByteAdr, cntBytes);
  80. return ParseBytes(varType, bytes, varCount, bitAdr);
  81. }
  82. /// <summary>
  83. /// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
  84. /// If the read was not successful, check LastErrorCode or LastErrorString.
  85. /// </summary>
  86. /// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
  87. /// <returns>Returns an object that contains the value. This object must be cast accordingly. If no data has been read, null will be returned</returns>
  88. public object? Read(string variable)
  89. {
  90. var adr = new PLCAddress(variable);
  91. return Read(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber);
  92. }
  93. public bool ReadBoolean(string variable)
  94. {
  95. var adr = new PLCAddress(variable);
  96. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  97. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes);
  98. return (((int)bytes[0] & (1 << adr.BitNumber)) != 0);
  99. }
  100. public byte ReadByte(string variable)
  101. {
  102. var adr = new PLCAddress(variable);
  103. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  104. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes);
  105. return bytes[0];
  106. }
  107. public sbyte ReadSByte(string variable)
  108. {
  109. var adr = new PLCAddress(variable);
  110. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  111. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray();
  112. return (sbyte)bytes[0];
  113. }
  114. public short ReadInt16(string variable)
  115. {
  116. var adr = new PLCAddress(variable);
  117. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  118. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray();
  119. #if NETFRAMEWORK
  120. return BitConverter.ToInt16(bytes, 0);
  121. #else
  122. return Unsafe.As<byte, short>(ref bytes[0]);
  123. #endif
  124. }
  125. public ushort ReadUInt16(string variable)
  126. {
  127. var adr = new PLCAddress(variable);
  128. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  129. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray();
  130. #if NETFRAMEWORK
  131. return BitConverter.ToUInt16(bytes, 0);
  132. #else
  133. return Unsafe.As<byte, ushort>(ref bytes[0]);
  134. #endif
  135. }
  136. public int ReadInt32(string variable)
  137. {
  138. var adr = new PLCAddress(variable);
  139. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  140. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray();
  141. #if NETFRAMEWORK
  142. return BitConverter.ToInt32(bytes, 0);
  143. #else
  144. return Unsafe.As<byte, int>(ref bytes[0]);
  145. #endif
  146. }
  147. public uint ReadUInt32(string variable)
  148. {
  149. var adr = new PLCAddress(variable);
  150. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  151. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray();
  152. #if NETFRAMEWORK
  153. return BitConverter.ToUInt32(bytes, 0);
  154. #else
  155. return Unsafe.As<byte, uint>(ref bytes[0]);
  156. #endif
  157. }
  158. public long ReadInt64(string variable)
  159. {
  160. var adr = new PLCAddress(variable);
  161. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  162. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray();
  163. #if NETFRAMEWORK
  164. return BitConverter.ToInt64(bytes, 0);
  165. #else
  166. return Unsafe.As<byte, long>(ref bytes[0]);
  167. #endif
  168. }
  169. public ulong ReadUInt64(string variable)
  170. {
  171. var adr = new PLCAddress(variable);
  172. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  173. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray();
  174. #if NETFRAMEWORK
  175. return BitConverter.ToUInt64(bytes, 0);
  176. #else
  177. return Unsafe.As<byte, ulong>(ref bytes[0]);
  178. #endif
  179. }
  180. public float ReadFloat(string variable)
  181. {
  182. var adr = new PLCAddress(variable);
  183. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  184. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray();
  185. #if NETFRAMEWORK
  186. return BitConverter.ToSingle(bytes, 0);
  187. #else
  188. return Unsafe.As<byte, float>(ref bytes[0]);
  189. #endif
  190. }
  191. public double ReadDouble(string variable)
  192. {
  193. var adr = new PLCAddress(variable);
  194. int cntBytes = VarTypeToByteLength(adr.VarType, 1);
  195. byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray();
  196. #if NETFRAMEWORK
  197. return BitConverter.ToDouble(bytes, 0);
  198. #else
  199. return Unsafe.As<byte, double>(ref bytes[0]);
  200. #endif
  201. }
  202. /// <summary>
  203. /// 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.
  204. /// </summary>
  205. /// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
  206. /// <param name="db">Address of the DB.</param>
  207. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  208. /// <returns>Returns a struct that must be cast. If no data has been read, null will be returned</returns>
  209. public object? ReadStruct(Type structType, int db, int startByteAdr = 0)
  210. {
  211. int numBytes = Struct.GetStructSize(structType);
  212. // now read the package
  213. var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
  214. // and decode it
  215. return Struct.FromBytes(structType, resultBytes);
  216. }
  217. /// <summary>
  218. /// 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.
  219. /// </summary>
  220. /// <typeparam name="T">The struct type</typeparam>
  221. /// <param name="db">Address of the DB.</param>
  222. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  223. /// <returns>Returns a nullable struct. If nothing was read null will be returned.</returns>
  224. public T? ReadStruct<T>(int db, int startByteAdr = 0) where T : struct
  225. {
  226. return ReadStruct(typeof(T), db, startByteAdr) as T?;
  227. }
  228. /// <summary>
  229. /// 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.
  230. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
  231. /// </summary>
  232. /// <param name="sourceClass">Instance of the class that will store the values</param>
  233. /// <param name="db">Index of the DB; es.: 1 is for DB1</param>
  234. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  235. /// <returns>The number of read bytes</returns>
  236. public int ReadClass(object sourceClass, int db, int startByteAdr = 0)
  237. {
  238. int numBytes = (int)Class.GetClassSize(sourceClass);
  239. if (numBytes <= 0)
  240. {
  241. throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
  242. }
  243. // now read the package
  244. var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
  245. // and decode it
  246. Class.FromBytes(sourceClass, resultBytes);
  247. return resultBytes.Length;
  248. }
  249. /// <summary>
  250. /// 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.
  251. /// 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
  252. /// type, the class needs a default constructor.
  253. /// </summary>
  254. /// <typeparam name="T">The class that will be instantiated. Requires a default constructor</typeparam>
  255. /// <param name="db">Index of the DB; es.: 1 is for DB1</param>
  256. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  257. /// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
  258. public T? ReadClass<T>(int db, int startByteAdr = 0) where T : class
  259. {
  260. return ReadClass(() => Activator.CreateInstance<T>(), db, startByteAdr);
  261. }
  262. /// <summary>
  263. /// 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.
  264. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
  265. /// </summary>
  266. /// <typeparam name="T">The class that will be instantiated</typeparam>
  267. /// <param name="classFactory">Function to instantiate the class</param>
  268. /// <param name="db">Index of the DB; es.: 1 is for DB1</param>
  269. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  270. /// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
  271. public T? ReadClass<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
  272. {
  273. var instance = classFactory();
  274. int readBytes = ReadClass(instance, db, startByteAdr);
  275. if (readBytes <= 0)
  276. {
  277. return null;
  278. }
  279. return instance;
  280. }
  281. /// <summary>
  282. /// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
  283. /// If the write was not successful, check LastErrorCode or LastErrorString.
  284. /// </summary>
  285. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  286. /// <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>
  287. /// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
  288. /// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
  289. public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value)
  290. {
  291. WriteBytes(dataType, db, startByteAdr, value.AsSpan());
  292. }
  293. /// <summary>
  294. /// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
  295. /// If the write was not successful, check LastErrorCode or LastErrorString.
  296. /// </summary>
  297. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  298. /// <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>
  299. /// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
  300. /// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
  301. public void WriteBytes(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
  302. {
  303. int localIndex = 0;
  304. while (value.Length > 0)
  305. {
  306. //TODO: Figure out how to use MaxPDUSize here
  307. //Snap7 seems to choke on PDU sizes above 256 even if snap7
  308. //replies with bigger PDU size in connection setup.
  309. var maxToWrite = Math.Min(value.Length, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
  310. WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite));
  311. value = value.Slice(maxToWrite);
  312. localIndex += maxToWrite;
  313. }
  314. }
  315. /// <summary>
  316. /// Write a single bit from a DB with the specified index.
  317. /// </summary>
  318. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  319. /// <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>
  320. /// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
  321. /// <param name="bitAdr">The address of the bit. (0-7)</param>
  322. /// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
  323. public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, bool value)
  324. {
  325. if (bitAdr < 0 || bitAdr > 7)
  326. throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr));
  327. WriteBitWithASingleRequest(dataType, db, startByteAdr, bitAdr, value);
  328. }
  329. /// <summary>
  330. /// Write a single bit to a DB with the specified index.
  331. /// </summary>
  332. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  333. /// <param name="db">Address of the memory area (if you want to write DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
  334. /// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
  335. /// <param name="bitAdr">The address of the bit. (0-7)</param>
  336. /// <param name="value">Value to write (0 or 1).</param>
  337. public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, int value)
  338. {
  339. if (value < 0 || value > 1)
  340. throw new ArgumentException("Value must be 0 or 1", nameof(value));
  341. WriteBit(dataType, db, startByteAdr, bitAdr, value == 1);
  342. }
  343. /// <summary>
  344. /// 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.
  345. /// You must specify the memory area type, memory are address, byte start address and bytes count.
  346. /// If the read was not successful, check LastErrorCode or LastErrorString.
  347. /// </summary>
  348. /// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
  349. /// <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>
  350. /// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
  351. /// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
  352. /// <param name="bitAdr">The address of the bit. (0-7)</param>
  353. public void Write(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1)
  354. {
  355. if (bitAdr != -1)
  356. {
  357. //Must be writing a bit value as bitAdr is specified
  358. if (value is bool boolean)
  359. {
  360. WriteBit(dataType, db, startByteAdr, bitAdr, boolean);
  361. }
  362. else if (value is int intValue)
  363. {
  364. if (intValue < 0 || intValue > 7)
  365. throw new ArgumentOutOfRangeException(
  366. string.Format(
  367. "Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid",
  368. bitAdr), nameof(bitAdr));
  369. WriteBit(dataType, db, startByteAdr, bitAdr, intValue == 1);
  370. }
  371. else
  372. throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
  373. }
  374. else WriteBytes(dataType, db, startByteAdr, Serialization.SerializeValue(value));
  375. }
  376. /// <summary>
  377. /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
  378. /// </summary>
  379. /// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
  380. /// <param name="value">Value to be written to the PLC</param>
  381. public void Write(string variable, object value)
  382. {
  383. var adr = new PLCAddress(variable);
  384. Write(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber);
  385. }
  386. /// <summary>
  387. /// Writes a C# struct to a DB in the PLC
  388. /// </summary>
  389. /// <param name="structValue">The struct to be written</param>
  390. /// <param name="db">Db address</param>
  391. /// <param name="startByteAdr">Start bytes on the PLC</param>
  392. public void WriteStruct(object structValue, int db, int startByteAdr = 0)
  393. {
  394. WriteStructAsync(structValue, db, startByteAdr).GetAwaiter().GetResult();
  395. }
  396. /// <summary>
  397. /// Writes a C# class to a DB in the PLC
  398. /// </summary>
  399. /// <param name="classValue">The class to be written</param>
  400. /// <param name="db">Db address</param>
  401. /// <param name="startByteAdr">Start bytes on the PLC</param>
  402. public void WriteClass(object classValue, int db, int startByteAdr = 0)
  403. {
  404. WriteClassAsync(classValue, db, startByteAdr).GetAwaiter().GetResult();
  405. }
  406. private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, Span<byte> buffer)
  407. {
  408. try
  409. {
  410. // first create the header
  411. const int packageSize = 19 + 12; // 19 header + 12 for 1 request
  412. var dataToSend = new byte[packageSize];
  413. var package = new MemoryStream(dataToSend);
  414. WriteReadHeader(package);
  415. // package.Add(0x02); // datenart
  416. BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length);
  417. var s7data = RequestTsdu(dataToSend);
  418. AssertReadResponse(s7data, buffer.Length);
  419. s7data.AsSpan(18, buffer.Length).CopyTo(buffer);
  420. }
  421. catch (Exception exc)
  422. {
  423. throw new PlcException(ErrorCode.ReadData, exc);
  424. }
  425. }
  426. /// <summary>
  427. /// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
  428. /// or when the PLC reports errors for item(s) written.
  429. /// </summary>
  430. /// <param name="dataItems">The DataItem(s) to write to the PLC.</param>
  431. public void Write(params DataItem[] dataItems)
  432. {
  433. AssertPduSizeForWrite(dataItems);
  434. var message = new ByteArray();
  435. var length = S7WriteMultiple.CreateRequest(message, dataItems);
  436. var response = RequestTsdu(message.Array, 0, length);
  437. S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
  438. }
  439. private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
  440. {
  441. try
  442. {
  443. var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value);
  444. var s7data = RequestTsdu(dataToSend);
  445. ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
  446. }
  447. catch (Exception exc)
  448. {
  449. throw new PlcException(ErrorCode.WriteData, exc);
  450. }
  451. }
  452. private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
  453. {
  454. int varCount = value.Length;
  455. // first create the header
  456. int packageSize = 35 + varCount;
  457. var packageData = new byte[packageSize];
  458. var package = new MemoryStream(packageData);
  459. package.WriteByte(3);
  460. package.WriteByte(0);
  461. //complete package size
  462. package.Write(Int.ToByteArray((short)packageSize));
  463. // This overload doesn't allocate the byte array, it refers to assembly's static data segment
  464. package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
  465. package.Write(Word.ToByteArray((ushort)(varCount - 1)));
  466. package.Write(new byte[] { 0, 0x0e });
  467. package.Write(Word.ToByteArray((ushort)(varCount + 4)));
  468. package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
  469. package.Write(Word.ToByteArray((ushort)varCount));
  470. package.Write(Word.ToByteArray((ushort)(db)));
  471. package.WriteByte((byte)dataType);
  472. var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
  473. package.WriteByte((byte)overflow);
  474. package.Write(Word.ToByteArray((ushort)(startByteAdr * 8)));
  475. package.Write(new byte[] { 0, 4 });
  476. package.Write(Word.ToByteArray((ushort)(varCount * 8)));
  477. // now join the header and the data
  478. package.Write(value);
  479. return packageData;
  480. }
  481. private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr)
  482. {
  483. var value = new[] { bitValue ? (byte)1 : (byte)0 };
  484. int varCount = 1;
  485. // first create the header
  486. int packageSize = 35 + varCount;
  487. var packageData = new byte[packageSize];
  488. var package = new MemoryStream(packageData);
  489. package.WriteByte(3);
  490. package.WriteByte(0);
  491. //complete package size
  492. package.Write(Int.ToByteArray((short)packageSize));
  493. package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
  494. package.Write(Word.ToByteArray((ushort)(varCount - 1)));
  495. package.Write(new byte[] { 0, 0x0e });
  496. package.Write(Word.ToByteArray((ushort)(varCount + 4)));
  497. package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit
  498. package.Write(Word.ToByteArray((ushort)varCount));
  499. package.Write(Word.ToByteArray((ushort)(db)));
  500. package.WriteByte((byte)dataType);
  501. var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
  502. package.WriteByte((byte)overflow);
  503. package.Write(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr)));
  504. package.Write(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit
  505. package.Write(Word.ToByteArray((ushort)(varCount)));
  506. // now join the header and the data
  507. package.Write(value);
  508. return packageData;
  509. }
  510. private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
  511. {
  512. try
  513. {
  514. var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
  515. var s7data = RequestTsdu(dataToSend);
  516. ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
  517. }
  518. catch (Exception exc)
  519. {
  520. throw new PlcException(ErrorCode.WriteData, exc);
  521. }
  522. }
  523. /// <summary>
  524. /// Reads multiple vars in a single request.
  525. /// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
  526. /// Values are stored in the property "Value" of the dataItem and are already converted.
  527. /// If you don't want the conversion, just create a dataItem of bytes.
  528. /// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
  529. /// </summary>
  530. /// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
  531. public void ReadMultipleVars(List<DataItem> dataItems)
  532. {
  533. AssertPduSizeForRead(dataItems);
  534. try
  535. {
  536. // first create the header
  537. int packageSize = 19 + (dataItems.Count * 12);
  538. var dataToSend = new byte[packageSize];
  539. var package = new MemoryStream(dataToSend);
  540. WriteReadHeader(package, dataItems.Count);
  541. // package.Add(0x02); // datenart
  542. foreach (var dataItem in dataItems)
  543. {
  544. BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count));
  545. }
  546. byte[] s7data = RequestTsdu(dataToSend);
  547. ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
  548. ParseDataIntoDataItems(s7data, dataItems);
  549. }
  550. catch (Exception exc)
  551. {
  552. throw new PlcException(ErrorCode.ReadData, exc);
  553. }
  554. }
  555. /// <summary>
  556. /// Read the PLC clock value.
  557. /// </summary>
  558. /// <returns>The current PLC time.</returns>
  559. public System.DateTime ReadClock()
  560. {
  561. var request = BuildClockReadRequest();
  562. var response = RequestTsdu(request);
  563. return ParseClockReadResponse(response);
  564. }
  565. /// <summary>
  566. /// Write the PLC clock value.
  567. /// </summary>
  568. /// <param name="value">The date and time to set the PLC clock to.</param>
  569. public void WriteClock(System.DateTime value)
  570. {
  571. var request = BuildClockWriteRequest(value);
  572. var response = RequestTsdu(request);
  573. ParseClockWriteResponse(response);
  574. }
  575. /// <summary>
  576. /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
  577. /// </summary>
  578. /// <returns>The current PLC status.</returns>
  579. public byte ReadStatus()
  580. {
  581. var dataToSend = BuildSzlReadRequestPackage(0x0424, 0);
  582. var s7data = RequestTsdu(dataToSend);
  583. return (byte) (s7data[37] & 0x0f);
  584. }
  585. private byte[] RequestTsdu(byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length);
  586. private byte[] RequestTsdu(byte[] requestData, int offset, int length)
  587. {
  588. return RequestTsduAsync(requestData, offset, length).GetAwaiter().GetResult();
  589. }
  590. }
  591. }