using S7.Net.Types; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Net.Sockets; using System.Threading.Tasks; //Implement obsolete synchronous methods here namespace S7.Net { public partial class Plc { /// /// Open a and connects to the PLC, sending all the corrected package /// and returning if the connection was successful () of it was wrong. /// /// Returns ErrorCode.NoError if the connection was successful, otherwise check the ErrorCode public ErrorCode Open() { if (Connect() != ErrorCode.NoError) { return LastErrorCode; } try { stream.Write(GetCOPTConnectionRequest(CPU), 0, 22); var response = COTP.TPDU.Read(stream); if (response.PDUType != 0xd0) //Connect Confirm { throw new WrongNumberOfBytesException("Waiting for COTP connect confirm"); } stream.Write(GetS7ConnectionSetup(), 0, 25); var s7data = COTP.TSDU.Read(stream); if (s7data == null || s7data[1] != 0x03) //Check for S7 Ack Data { throw new WrongNumberOfBytesException("Waiting for S7 connection setup"); } MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]); } catch (Exception exc) { LastErrorCode = ErrorCode.ConnectionError; LastErrorString = string.Format("Couldn't establish the connection to {0}.\nMessage: {1}", IP, exc.Message); return ErrorCode.ConnectionError; } return ErrorCode.NoError; } private ErrorCode Connect() { try { tcpClient = new TcpClient(); tcpClient.Connect(IP, 102); stream = tcpClient.GetStream(); } catch (SocketException sex) { // see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668(v=vs.85).aspx if (sex.SocketErrorCode == SocketError.TimedOut) { LastErrorCode = ErrorCode.IPAddressNotAvailable; } else { LastErrorCode = ErrorCode.ConnectionError; } LastErrorString = sex.Message; } catch (Exception ex) { LastErrorCode = ErrorCode.ConnectionError; LastErrorString = ex.Message; } return LastErrorCode; } /// /// 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) { List resultBytes = new List(); int index = startByteAdr; while (count > 0) { //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0. var maxToRead = (int)Math.Min(count, MaxPDUSize - 18); byte[] bytes = ReadBytesWithSingleRequest(dataType, db, index, maxToRead); if (bytes == null) return resultBytes.ToArray(); resultBytes.AddRange(bytes); count -= maxToRead; index += maxToRead; } return resultBytes.ToArray(); } /// /// 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. public object Read(string variable) { var adr = new PLCAddress(variable); return Read(adr.dataType, adr.DBNumber, adr.Address, 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. 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 = 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. /// NoError if it was successful, or the error is specified public ErrorCode 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 = (int)Math.Min(count, 200); ErrorCode lastError = WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Skip(localIndex).Take(maxToWrite).ToArray()); if (lastError != ErrorCode.NoError) { return lastError; } count -= maxToWrite; localIndex += maxToWrite; } return ErrorCode.NoError; } /// /// 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. /// NoError if it was successful, or the error is specified public ErrorCode 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)); ErrorCode lastError = WriteBitWithASingleRequest(dataType, db, startByteAdr, bitAdr, value); if (lastError != ErrorCode.NoError) { return lastError; } return ErrorCode.NoError; } /// /// 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. /// NoError if it was successful, or the error is specified public ErrorCode 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)); return 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) /// NoError if it was successful, or the error is specified public ErrorCode 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) { return WriteBit(dataType, db, startByteAdr, bitAdr, (bool)value); } 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)); return WriteBit(dataType, db, startByteAdr, bitAdr, intValue == 1); } throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value)); } return WriteBytes(dataType, db, startByteAdr, GetPackage(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 /// NoError if it was successful, or the error is specified public ErrorCode Write(string variable, object value) { var adr = new PLCAddress(variable); return Write(adr.dataType, adr.DBNumber, adr.Address, 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 /// NoError if it was successful, or the error is specified public ErrorCode WriteStruct(object structValue, int db, int startByteAdr = 0) { return WriteStructAsync(structValue, db, startByteAdr).Result; } /// /// Writes a C# class to a DB in the PLC /// /// The class to be written /// Db address /// Start bytes on the PLC /// NoError if it was successful, or the error is specified public ErrorCode WriteClass(object classValue, int db, int startByteAdr = 0) { return WriteClassAsync(classValue, db, startByteAdr).Result; } private byte[] ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, int count) { byte[] bytes = new byte[count]; try { // first create the header int packageSize = 31; ByteArray package = new ByteArray(packageSize); package.Add(ReadHeaderPackage()); // package.Add(0x02); // datenart package.Add(CreateReadDataRequestPackage(dataType, db, startByteAdr, count)); stream.Write(package.array, 0, package.array.Length); var s7data = COTP.TSDU.Read(stream); if (s7data == null || s7data[14] != 0xff) throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString()); for (int cnt = 0; cnt < count; cnt++) bytes[cnt] = s7data[cnt + 18]; return bytes; } catch (SocketException socketException) { LastErrorCode = ErrorCode.WriteData; LastErrorString = socketException.Message; return null; } catch (Exception exc) { LastErrorCode = ErrorCode.WriteData; LastErrorString = exc.Message; return null; } } private ErrorCode WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value) { return WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr, value).Result; } private ErrorCode WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue) { return WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, bitValue).Result; } /// /// 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. /// DataItems must not be more than 20 (protocol restriction) and bytes must not be more than 200 + 22 of header (protocol restriction). /// /// List of dataitems that contains the list of variables that must be read. Maximum 20 dataitems are accepted. [Obsolete("Use ReadMultipleVarsAsync. Note: different function signature")] public void ReadMultipleVars(List dataItems) { int cntBytes = dataItems.Sum(dataItem => VarTypeToByteLength(dataItem.VarType, dataItem.Count)); //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. if (dataItems.Count > 20) throw new Exception("Too many vars requested"); if (cntBytes > 222) throw new Exception("Too many bytes requested"); // TODO: proper TDU check + split in multiple requests try { // first create the header int packageSize = 19 + (dataItems.Count * 12); Types.ByteArray package = new ByteArray(packageSize); package.Add(ReadHeaderPackage(dataItems.Count)); // package.Add(0x02); // datenart foreach (var dataItem in dataItems) { package.Add(CreateReadDataRequestPackage(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count))); } stream.Write(package.array, 0, package.array.Length); var s7data = COTP.TSDU.Read(stream); //TODO use Async if (s7data == null || s7data[14] != 0xff) throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString()); int offset = 18; foreach (var dataItem in dataItems) { int byteCnt = VarTypeToByteLength(dataItem.VarType, dataItem.Count); byte[] bytes = new byte[byteCnt]; for (int i = 0; i < byteCnt; i++) { bytes[i] = s7data[i + offset]; } offset += byteCnt + 4; dataItem.Value = ParseBytes(dataItem.VarType, bytes, dataItem.Count); } } catch (SocketException socketException) { LastErrorCode = ErrorCode.WriteData; LastErrorString = socketException.Message; } catch (Exception exc) { LastErrorCode = ErrorCode.WriteData; LastErrorString = exc.Message; } } } }