using S7.Net.Types; using System; using System.IO; using System.Collections.Generic; using S7.Net.Protocol; using S7.Net.Helper; using System.Runtime.CompilerServices; using System.Linq; //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]; ReadBytes(result, dataType, db, startByteAdr); return result; } /// /// 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. /// /// Buffer to receive the read bytes. The determines the number of bytes to read. /// 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. /// Returns the bytes in an array public void ReadBytes(Span buffer, DataType dataType, int db, int startByteAdr) { int index = 0; while (buffer.Length > 0) { //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0. var maxToRead = Math.Min(buffer.Length, MaxPDUSize - 18); ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead)); buffer = buffer.Slice(maxToRead); index += maxToRead; } } /// /// 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); } public bool ReadBoolean(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes); return (((int)bytes[0] & (1 << adr.BitNumber)) != 0); } public byte ReadByte(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes); return bytes[0]; } public sbyte ReadSByte(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray(); return (sbyte)bytes[0]; } public short ReadInt16(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray(); #if NETFRAMEWORK return BitConverter.ToInt16(bytes, 0); #else return Unsafe.As(ref bytes[0]); #endif } public ushort ReadUInt16(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray(); #if NETFRAMEWORK return BitConverter.ToUInt16(bytes, 0); #else return Unsafe.As(ref bytes[0]); #endif } public int ReadInt32(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray(); #if NETFRAMEWORK return BitConverter.ToInt32(bytes, 0); #else return Unsafe.As(ref bytes[0]); #endif } public uint ReadUInt32(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray(); #if NETFRAMEWORK return BitConverter.ToUInt32(bytes, 0); #else return Unsafe.As(ref bytes[0]); #endif } public long ReadInt64(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray(); #if NETFRAMEWORK return BitConverter.ToInt64(bytes, 0); #else return Unsafe.As(ref bytes[0]); #endif } public ulong ReadUInt64(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray(); #if NETFRAMEWORK return BitConverter.ToUInt64(bytes, 0); #else return Unsafe.As(ref bytes[0]); #endif } public float ReadFloat(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray(); #if NETFRAMEWORK return BitConverter.ToSingle(bytes, 0); #else return Unsafe.As(ref bytes[0]); #endif } public double ReadDouble(string variable) { var adr = new PLCAddress(variable); int cntBytes = VarTypeToByteLength(adr.VarType, 1); byte[] bytes = ReadBytes(adr.DataType, adr.DbNumber, adr.StartByte, cntBytes).Reverse().ToArray(); #if NETFRAMEWORK return BitConverter.ToDouble(bytes, 0); #else return Unsafe.As(ref bytes[0]); #endif } /// /// 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) { WriteBytes(dataType, db, startByteAdr, value.AsSpan()); } /// /// 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, ReadOnlySpan value) { int localIndex = 0; while (value.Length > 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(value.Length, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480 WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite)); value = value.Slice(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. /// /// 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, Span buffer) { try { // first create the header const int packageSize = 19 + 12; // 19 header + 12 for 1 request var dataToSend = new byte[packageSize]; var package = new MemoryStream(dataToSend); WriteReadHeader(package); // package.Add(0x02); // datenart BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length); var s7data = RequestTsdu(dataToSend); AssertReadResponse(s7data, buffer.Length); s7data.AsSpan(18, buffer.Length).CopyTo(buffer); } 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, ReadOnlySpan value) { try { var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value); 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, ReadOnlySpan value) { int varCount = value.Length; // first create the header int packageSize = 35 + varCount; var packageData = new byte[packageSize]; var package = new MemoryStream(packageData); package.WriteByte(3); package.WriteByte(0); //complete package size package.Write(Int.ToByteArray((short)packageSize)); // This overload doesn't allocate the byte array, it refers to assembly's static data segment package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 }); package.Write(Word.ToByteArray((ushort)(varCount - 1))); package.Write(new byte[] { 0, 0x0e }); package.Write(Word.ToByteArray((ushort)(varCount + 4))); package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 }); package.Write(Word.ToByteArray((ushort)varCount)); package.Write(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.Write(Word.ToByteArray((ushort)(startByteAdr * 8))); package.Write(new byte[] { 0, 4 }); package.Write(Word.ToByteArray((ushort)(varCount * 8))); // now join the header and the data package.Write(value); return packageData; } 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 packageData = new byte[packageSize]; var package = new MemoryStream(packageData); package.WriteByte(3); package.WriteByte(0); //complete package size package.Write(Int.ToByteArray((short)packageSize)); package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 }); package.Write(Word.ToByteArray((ushort)(varCount - 1))); package.Write(new byte[] { 0, 0x0e }); package.Write(Word.ToByteArray((ushort)(varCount + 4))); package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit package.Write(Word.ToByteArray((ushort)varCount)); package.Write(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.Write(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr))); package.Write(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit package.Write(Word.ToByteArray((ushort)(varCount))); // now join the header and the data package.Write(value); return packageData; } 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 dataToSend = new byte[packageSize]; var package = new MemoryStream(dataToSend); WriteReadHeader(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)); } byte[] s7data = RequestTsdu(dataToSend); ValidateResponseCode((ReadWriteErrorCode)s7data[14]); ParseDataIntoDataItems(s7data, dataItems); } catch (Exception exc) { throw new PlcException(ErrorCode.ReadData, exc); } } /// /// Read the PLC clock value. /// /// The current PLC time. public System.DateTime ReadClock() { var request = BuildClockReadRequest(); var response = RequestTsdu(request); return ParseClockReadResponse(response); } /// /// Write the PLC clock value. /// /// The date and time to set the PLC clock to. public void WriteClock(System.DateTime value) { var request = BuildClockWriteRequest(value); var response = RequestTsdu(request); ParseClockWriteResponse(response); } /// /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. /// /// The current PLC status. public byte ReadStatus() { var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); var s7data = RequestTsdu(dataToSend); return (byte) (s7data[37] & 0x0f); } 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(); } } }