using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Sockets; using System.Runtime.InteropServices; using S7.Net.Internal; using S7.Net.Protocol; using S7.Net.Types; namespace S7.Net { /// /// Creates an instance of S7.Net driver /// public partial class Plc : IDisposable { /// /// The default port for the S7 protocol. /// public const int DefaultPort = 102; /// /// The default timeout (in milliseconds) used for and . /// public const int DefaultTimeout = 10_000; private readonly TaskQueue queue = new TaskQueue(); //TCP connection to device private TcpClient? tcpClient; private NetworkStream? _stream; private int readTimeout = DefaultTimeout; // default no timeout private int writeTimeout = DefaultTimeout; // default no timeout /// /// IP address of the PLC /// public string IP { get; } /// /// PORT Number of the PLC, default is 102 /// public int Port { get; } /// /// The TSAP addresses used during the connection request. /// public TsapPair TsapPair { get; set; } /// /// CPU type of the PLC /// public CpuType CPU { get; } /// /// Rack of the PLC /// public Int16 Rack { get; } /// /// Slot of the CPU of the PLC /// public Int16 Slot { get; } /// /// Max PDU size this cpu supports /// public int MaxPDUSize { get; private set; } /// Gets or sets the amount of time that a read operation blocks waiting for data from PLC. /// A that specifies the amount of time, in milliseconds, that will elapse before a read operation fails. The default value, , specifies that the read operation does not time out. public int ReadTimeout { get => readTimeout; set { readTimeout = value; if (tcpClient != null) tcpClient.ReceiveTimeout = readTimeout; } } /// Gets or sets the amount of time that a write operation blocks waiting for data to PLC. /// A that specifies the amount of time, in milliseconds, that will elapse before a write operation fails. The default value, , specifies that the write operation does not time out. public int WriteTimeout { get => writeTimeout; set { writeTimeout = value; if (tcpClient != null) tcpClient.SendTimeout = writeTimeout; } } /// /// Gets a value indicating whether a connection to the PLC has been established. /// /// /// The property gets the connection state of the Client socket as /// of the last I/O operation. When it returns false, the Client socket was either /// never connected, or is no longer connected. /// /// /// Because the property only reflects the state of the connection /// as of the most recent operation, you should attempt to send or receive a message to /// determine the current state. After the message send fails, this property no longer /// returns true. Note that this behavior is by design. You cannot reliably test the /// state of the connection because, in the time between the test and a send/receive, the /// connection could have been lost. Your code should assume the socket is connected, and /// gracefully handle failed transmissions. /// /// public bool IsConnected => tcpClient?.Connected ?? false; /// /// Creates a PLC object with all the parameters needed for connections. /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. /// You need slot > 0 if you are connecting to external ethernet card (CP). /// For S7-300 and S7-400 the default is rack = 0 and slot = 2. /// /// CpuType of the PLC (select from the enum) /// Ip address of the PLC /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal /// slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500. /// If you use an external ethernet card, this must be set accordingly. public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot) : this(cpu, ip, DefaultPort, rack, slot) { } /// /// Creates a PLC object with all the parameters needed for connections. /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. /// You need slot > 0 if you are connecting to external ethernet card (CP). /// For S7-300 and S7-400 the default is rack = 0 and slot = 2. /// /// CpuType of the PLC (select from the enum) /// Ip address of the PLC /// Port number used for the connection, default 102. /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal /// slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500. /// If you use an external ethernet card, this must be set accordingly. public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot) : this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot)) { if (!Enum.IsDefined(typeof(CpuType), cpu)) throw new ArgumentException( $"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu)); CPU = cpu; Rack = rack; Slot = slot; } /// /// Creates a PLC object with all the parameters needed for connections. /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. /// You need slot > 0 if you are connecting to external ethernet card (CP). /// For S7-300 and S7-400 the default is rack = 0 and slot = 2. /// /// Ip address of the PLC /// The TSAP addresses used for the connection request. public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair) { } /// /// Creates a PLC object with all the parameters needed for connections. Use this constructor /// if you want to manually override the TSAP addresses used during the connection request. /// /// Ip address of the PLC /// Port number used for the connection, default 102. /// The TSAP addresses used for the connection request. public Plc(string ip, int port, TsapPair tsapPair) { if (string.IsNullOrEmpty(ip)) throw new ArgumentException("IP address must valid.", nameof(ip)); IP = ip; Port = port; MaxPDUSize = 240; TsapPair = tsapPair; } /// /// Close connection to PLC /// public void Close() { if (tcpClient != null) { if (tcpClient.Connected) tcpClient.Close(); tcpClient = null; // Can not reuse TcpClient once connection gets closed. } } private void AssertPduSizeForRead(ICollection dataItems) { // send request limit: 19 bytes of header data, 12 bytes of parameter data for each dataItem var requiredRequestSize = 19 + dataItems.Count * 12; if (requiredRequestSize > MaxPDUSize) throw new Exception($"Too many vars requested for read. Request size ({requiredRequestSize}) is larger than protocol limit ({MaxPDUSize})."); // response limit: 14 bytes of header data, 4 bytes of result data for each dataItem and the actual data var requiredResponseSize = GetDataLength(dataItems) + dataItems.Count * 4 + 14; if (requiredResponseSize > MaxPDUSize) throw new Exception($"Too much data requested for read. Response size ({requiredResponseSize}) is larger than protocol limit ({MaxPDUSize})."); } private void AssertPduSizeForWrite(ICollection dataItems) { // 12 bytes of header data, 18 bytes of parameter data for each dataItem if (dataItems.Count * 18 + 12 > MaxPDUSize) throw new Exception("Too many vars supplied for write"); // 12 bytes of header data, 16 bytes of data for each dataItem and the actual data if (GetDataLength(dataItems) + dataItems.Count * 16 + 12 > MaxPDUSize) throw new Exception("Too much data supplied for write"); } private void ConfigureConnection() { if (tcpClient == null) { return; } tcpClient.ReceiveTimeout = ReadTimeout; tcpClient.SendTimeout = WriteTimeout; } private int GetDataLength(IEnumerable dataItems) { // Odd length variables are 0-padded return dataItems.Select(di => VarTypeToByteLength(di.VarType, di.Count)) .Sum(len => (len & 1) == 1 ? len + 1 : len); } private static void AssertReadResponse(byte[] s7Data, int dataLength) { var expectedLength = dataLength + 18; PlcException NotEnoughBytes() => new PlcException(ErrorCode.WrongNumberReceivedBytes, $"Received {s7Data.Length} bytes: '{BitConverter.ToString(s7Data)}', expected {expectedLength} bytes.") ; if (s7Data == null) throw new PlcException(ErrorCode.WrongNumberReceivedBytes, "No s7Data received."); if (s7Data.Length < 15) throw NotEnoughBytes(); ValidateResponseCode((ReadWriteErrorCode)s7Data[14]); if (s7Data.Length < expectedLength) throw NotEnoughBytes(); } internal static void ValidateResponseCode(ReadWriteErrorCode statusCode) { switch (statusCode) { case ReadWriteErrorCode.ObjectDoesNotExist: throw new Exception("Received error from PLC: Object does not exist."); case ReadWriteErrorCode.DataTypeInconsistent: throw new Exception("Received error from PLC: Data type inconsistent."); case ReadWriteErrorCode.DataTypeNotSupported: throw new Exception("Received error from PLC: Data type not supported."); case ReadWriteErrorCode.AccessingObjectNotAllowed: throw new Exception("Received error from PLC: Accessing object not allowed."); case ReadWriteErrorCode.AddressOutOfRange: throw new Exception("Received error from PLC: Address out of range."); case ReadWriteErrorCode.HardwareFault: throw new Exception("Received error from PLC: Hardware fault."); case ReadWriteErrorCode.Success: break; default: throw new Exception( $"Invalid response from PLC: statusCode={(byte)statusCode}."); } } public int AutoClearCacheInterval { get; set; } = 1; private System.DateTime LastClearTime = System.DateTime.Now; private Stream GetStreamIfAvailable() { if (_stream == null) { throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected"); } if (AutoClearCacheInterval > 0 && (System.DateTime.Now - LastClearTime).TotalMinutes >= AutoClearCacheInterval) { ClearCache(); LastClearTime = System.DateTime.Now; } return _stream; } private void ClearCache() { GC.Collect(); GC.WaitForPendingFinalizers(); if (Environment.OSVersion.Platform == PlatformID.Win32NT) { SetProcessWorkingSetSize(Process.GetCurrentProcess().Id, -1, -1); } } [DllImport("kernel32.dll")] private static extern int SetProcessWorkingSetSize(int process, int minSize, int maxSize); #region IDisposable Support private bool disposedValue = false; // To detect redundant calls /// /// Dispose Plc Object /// /// protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { Close(); } // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. // TODO: set large fields to null. disposedValue = true; } } // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. // ~Plc() { // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. // Dispose(false); // } // This code added to correctly implement the disposable pattern. void IDisposable.Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); // TODO: uncomment the following line if the finalizer is overridden above. // GC.SuppressFinalize(this); } #endregion } }