using S7.Net.Types; using System; using System.IO; using System.Collections.Generic; using S7.Net.Protocol; using S7.Net.Helper; //Implement synchronous methods here namespace S7.Net { public partial class Plc { /// /// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup. /// public void Open() { try { OpenAsync().GetAwaiter().GetResult(); } catch (Exception exc) { throw new PlcException(ErrorCode.ConnectionError, $"Couldn't establish the connection to {IP}.\nMessage: {exc.Message}", exc); } } /// /// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. /// If the read was not successful, check LastErrorCode or LastErrorString. /// /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. /// 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. /// Start byte address. If you want to read DB1.DBW200, this is 200. /// Byte count, if you want to read 120 bytes, set this to 120. /// Returns the bytes in an array public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count) { var result = new byte[count]; int index = 0; while (count > 0) { //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0. var maxToRead = Math.Min(count, MaxPDUSize - 18); ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, result, index, maxToRead); count -= maxToRead; index += maxToRead; } return result; } /// /// Read and decode a certain number of bytes of the "VarType" provided. /// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc). /// If the read was not successful, check LastErrorCode or LastErrorString. /// /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. /// 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. /// Start byte address. If you want to read DB1.DBW200, this is 200. /// Type of the variable/s that you are reading /// Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter. /// public object? Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0) { int cntBytes = VarTypeToByteLength(varType, varCount); byte[] bytes = ReadBytes(dataType, db, startByteAdr, cntBytes); return ParseBytes(varType, bytes, varCount, bitAdr); } /// /// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// If the read was not successful, check LastErrorCode or LastErrorString. /// /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// Returns an object that contains the value. This object must be cast accordingly. If no data has been read, null will be returned public object? Read(string variable) { var adr = new PLCAddress(variable); return Read(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber); } /// /// 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. /// /// Type of the struct to be readed (es.: TypeOf(MyStruct)). /// Address of the DB. /// Start byte address. If you want to read DB1.DBW200, this is 200. /// Returns a struct that must be cast. If no data has been read, null will be returned public object? ReadStruct(Type structType, int db, int startByteAdr = 0) { int numBytes = Struct.GetStructSize(structType); // now read the package var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes); // and decode it return Struct.FromBytes(structType, resultBytes); } /// /// 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. /// /// The struct type /// Address of the DB. /// Start byte address. If you want to read DB1.DBW200, this is 200. /// Returns a nullable struct. If nothing was read null will be returned. public T? ReadStruct(int db, int startByteAdr = 0) where T : struct { return ReadStruct(typeof(T), db, startByteAdr) as T?; } /// /// 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. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. /// /// Instance of the class that will store the values /// Index of the DB; es.: 1 is for DB1 /// Start byte address. If you want to read DB1.DBW200, this is 200. /// The number of read bytes public int ReadClass(object sourceClass, int db, int startByteAdr = 0) { int numBytes = (int)Class.GetClassSize(sourceClass); if (numBytes <= 0) { throw new Exception("The size of the class is less than 1 byte and therefore cannot be read"); } // now read the package var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes); // and decode it Class.FromBytes(sourceClass, resultBytes); return resultBytes.Length; } /// /// 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. /// 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 /// type, the class needs a default constructor. /// /// The class that will be instantiated. Requires a default constructor /// Index of the DB; es.: 1 is for DB1 /// Start byte address. If you want to read DB1.DBW200, this is 200. /// An instance of the class with the values read from the PLC. If no data has been read, null will be returned public T? ReadClass(int db, int startByteAdr = 0) where T : class { return ReadClass(() => Activator.CreateInstance(), db, startByteAdr); } /// /// 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. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. /// /// The class that will be instantiated /// Function to instantiate the class /// Index of the DB; es.: 1 is for DB1 /// Start byte address. If you want to read DB1.DBW200, this is 200. /// An instance of the class with the values read from the PLC. If no data has been read, null will be returned public T? ReadClass(Func classFactory, int db, int startByteAdr = 0) where T : class { var instance = classFactory(); int readBytes = ReadClass(instance, db, startByteAdr); if (readBytes <= 0) { return null; } return instance; } /// /// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. /// If the write was not successful, check LastErrorCode or LastErrorString. /// /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. /// 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. /// Start byte address. If you want to write DB1.DBW200, this is 200. /// Bytes to write. If more than 200, multiple requests will be made. public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value) { int localIndex = 0; int count = value.Length; while (count > 0) { //TODO: Figure out how to use MaxPDUSize here //Snap7 seems to choke on PDU sizes above 256 even if snap7 //replies with bigger PDU size in connection setup. var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480 WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite); count -= maxToWrite; localIndex += maxToWrite; } } /// /// Write a single bit from a DB with the specified index. /// /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. /// 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. /// Start byte address. If you want to write DB1.DBW200, this is 200. /// The address of the bit. (0-7) /// Bytes to write. If more than 200, multiple requests will be made. public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, bool value) { if (bitAdr < 0 || bitAdr > 7) throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr)); WriteBitWithASingleRequest(dataType, db, startByteAdr, bitAdr, value); } /// /// Write a single bit to a DB with the specified index. /// /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. /// 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. /// Start byte address. If you want to write DB1.DBW200, this is 200. /// The address of the bit. (0-7) /// Value to write (0 or 1). public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, int value) { if (value < 0 || value > 1) throw new ArgumentException("Value must be 0 or 1", nameof(value)); WriteBit(dataType, db, startByteAdr, bitAdr, value == 1); } /// /// 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. /// You must specify the memory area type, memory are address, byte start address and bytes count. /// If the read was not successful, check LastErrorCode or LastErrorString. /// /// Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output. /// 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. /// Start byte address. If you want to read DB1.DBW200, this is 200. /// Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion. /// The address of the bit. (0-7) public void Write(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1) { if (bitAdr != -1) { //Must be writing a bit value as bitAdr is specified if (value is bool boolean) { WriteBit(dataType, db, startByteAdr, bitAdr, boolean); } else if (value is int intValue) { if (intValue < 0 || intValue > 7) throw new ArgumentOutOfRangeException( string.Format( "Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr), nameof(bitAdr)); WriteBit(dataType, db, startByteAdr, bitAdr, intValue == 1); } else throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value)); } else WriteBytes(dataType, db, startByteAdr, Serialization.SerializeValue(value)); } /// /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// If the write was not successful, check or . /// /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// Value to be written to the PLC public void Write(string variable, object value) { var adr = new PLCAddress(variable); Write(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber); } /// /// Writes a C# struct to a DB in the PLC /// /// The struct to be written /// Db address /// Start bytes on the PLC public void WriteStruct(object structValue, int db, int startByteAdr = 0) { WriteStructAsync(structValue, db, startByteAdr).GetAwaiter().GetResult(); } /// /// Writes a C# class to a DB in the PLC /// /// The class to be written /// Db address /// Start bytes on the PLC public void WriteClass(object classValue, int db, int startByteAdr = 0) { WriteClassAsync(classValue, db, startByteAdr).GetAwaiter().GetResult(); } private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count) { try { // first create the header int packageSize = 19 + 12; // 19 header + 12 for 1 request var package = new System.IO.MemoryStream(packageSize); BuildHeaderPackage(package); // package.Add(0x02); // datenart BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count); var dataToSend = package.ToArray(); var s7data = RequestTsdu(dataToSend); AssertReadResponse(s7data, count); Array.Copy(s7data, 18, buffer, offset, count); } catch (Exception exc) { throw new PlcException(ErrorCode.ReadData, exc); } } /// /// Write DataItem(s) to the PLC. Throws an exception if the response is invalid /// or when the PLC reports errors for item(s) written. /// /// The DataItem(s) to write to the PLC. public void Write(params DataItem[] dataItems) { AssertPduSizeForWrite(dataItems); var message = new ByteArray(); var length = S7WriteMultiple.CreateRequest(message, dataItems); var response = RequestTsdu(message.Array, 0, length); S7WriteMultiple.ParseResponse(response, response.Length, dataItems); } private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count) { try { var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count); var s7data = RequestTsdu(dataToSend); ValidateResponseCode((ReadWriteErrorCode)s7data[14]); } catch (Exception exc) { throw new PlcException(ErrorCode.WriteData, exc); } } private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count) { int varCount = count; // first create the header int packageSize = 35 + varCount; var package = new MemoryStream(new byte[packageSize]); package.WriteByte(3); package.WriteByte(0); //complete package size package.WriteByteArray(Int.ToByteArray((short)packageSize)); package.WriteByteArray(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 }); package.WriteByteArray(Word.ToByteArray((ushort)(varCount - 1))); package.WriteByteArray(new byte[] { 0, 0x0e }); package.WriteByteArray(Word.ToByteArray((ushort)(varCount + 4))); package.WriteByteArray(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 }); package.WriteByteArray(Word.ToByteArray((ushort)varCount)); package.WriteByteArray(Word.ToByteArray((ushort)(db))); package.WriteByte((byte)dataType); var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191 package.WriteByte((byte)overflow); package.WriteByteArray(Word.ToByteArray((ushort)(startByteAdr * 8))); package.WriteByteArray(new byte[] { 0, 4 }); package.WriteByteArray(Word.ToByteArray((ushort)(varCount * 8))); // now join the header and the data package.Write(value, dataOffset, count); return package.ToArray(); } private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr) { var value = new[] { bitValue ? (byte)1 : (byte)0 }; int varCount = 1; // first create the header int packageSize = 35 + varCount; var package = new MemoryStream(new byte[packageSize]); package.WriteByte(3); package.WriteByte(0); //complete package size package.WriteByteArray(Int.ToByteArray((short)packageSize)); package.WriteByteArray(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 }); package.WriteByteArray(Word.ToByteArray((ushort)(varCount - 1))); package.WriteByteArray(new byte[] { 0, 0x0e }); package.WriteByteArray(Word.ToByteArray((ushort)(varCount + 4))); package.WriteByteArray(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit package.WriteByteArray(Word.ToByteArray((ushort)varCount)); package.WriteByteArray(Word.ToByteArray((ushort)(db))); package.WriteByte((byte)dataType); var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191 package.WriteByte((byte)overflow); package.WriteByteArray(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr))); package.WriteByteArray(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit package.WriteByteArray(Word.ToByteArray((ushort)(varCount))); // now join the header and the data package.WriteByteArray(value); return package.ToArray(); } private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue) { try { var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr); var s7data = RequestTsdu(dataToSend); ValidateResponseCode((ReadWriteErrorCode)s7data[14]); } catch (Exception exc) { throw new PlcException(ErrorCode.WriteData, exc); } } /// /// Reads multiple vars in a single request. /// You have to create and pass a list of DataItems and you obtain in response the same list with the values. /// Values are stored in the property "Value" of the dataItem and are already converted. /// If you don't want the conversion, just create a dataItem of bytes. /// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction). /// /// List of dataitems that contains the list of variables that must be read. public void ReadMultipleVars(List dataItems) { AssertPduSizeForRead(dataItems); try { // first create the header int packageSize = 19 + (dataItems.Count * 12); var package = new System.IO.MemoryStream(packageSize); BuildHeaderPackage(package, dataItems.Count); // package.Add(0x02); // datenart foreach (var dataItem in dataItems) { BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count)); } var dataToSend = package.ToArray(); var s7data = RequestTsdu(dataToSend); ValidateResponseCode((ReadWriteErrorCode)s7data[14]); ParseDataIntoDataItems(s7data, dataItems); } catch (Exception exc) { throw new PlcException(ErrorCode.ReadData, exc); } } private byte[] RequestTsdu(byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length); private byte[] RequestTsdu(byte[] requestData, int offset, int length) { return RequestTsduAsync(requestData, offset, length).GetAwaiter().GetResult(); } } }