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
}
}