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