初始化上传

This commit is contained in:
2025-08-26 08:37:44 +08:00
commit 31d81b91b6
448 changed files with 80981 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// COTP Protocol functions and types
/// </summary>
internal class COTP
{
public enum PduType : byte
{
Data = 0xf0,
ConnectionConfirmed = 0xd0
}
/// <summary>
/// Describes a COTP TPDU (Transport protocol data unit)
/// </summary>
public class TPDU
{
public TPKT TPkt { get; }
public byte HeaderLength;
public PduType PDUType;
public int TPDUNumber;
public byte[] Data;
public bool LastDataUnit;
public TPDU(TPKT tPKT)
{
TPkt = tPKT;
HeaderLength = tPKT.Data[0]; // Header length excluding this length byte
if (HeaderLength >= 2)
{
PDUType = (PduType)tPKT.Data[1];
if (PDUType == PduType.Data) //DT Data
{
var flags = tPKT.Data[2];
TPDUNumber = flags & 0x7F;
LastDataUnit = (flags & 0x80) > 0;
Data = new byte[tPKT.Data.Length - HeaderLength - 1]; // substract header length byte + header length.
Array.Copy(tPKT.Data, HeaderLength + 1, Data, 0, Data.Length);
return;
}
//TODO: Handle other PDUTypes
}
Data = new byte[0];
}
/// <summary>
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <param name="stream">The socket to read from</param>
/// <returns>COTP DPDU instance</returns>
public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var tpkt = await TPKT.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (tpkt.Length == 0)
{
throw new TPDUInvalidException("No protocol data received");
}
return new TPDU(tpkt);
}
public override string ToString()
{
return string.Format("Length: {0} PDUType: {1} TPDUNumber: {2} Last: {3} Segment Data: {4}",
HeaderLength,
PDUType,
TPDUNumber,
LastDataUnit,
BitConverter.ToString(Data)
);
}
}
/// <summary>
/// Describes a COTP TSDU (Transport service data unit). One TSDU consist of 1 ore more TPDUs
/// </summary>
public class TSDU
{
/// <summary>
/// Reads the full COTP TSDU (Transport service data unit)
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <returns>Data in TSDU</returns>
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (segment.LastDataUnit)
{
return segment.Data;
}
// More segments are expected, prepare a buffer to store all data
var buffer = new byte[segment.Data.Length];
Array.Copy(segment.Data, buffer, segment.Data.Length);
while (!segment.LastDataUnit)
{
segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var previousLength = buffer.Length;
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length);
}
return buffer;
}
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Net.Sockets;
namespace S7.Net
{
public static class TcpClientMixins
{
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Globalization;
namespace S7.Net
{
/// <summary>
/// Conversion methods to convert from Siemens numeric format to C# and back
/// </summary>
public static class Conversion
{
/// <summary>
/// Converts a binary string to Int32 value
/// </summary>
/// <param name="txt"></param>
/// <returns></returns>
public static int BinStringToInt32(this string txt)
{
int ret = 0;
for (int i = 0; i < txt.Length; i++)
{
ret = (ret << 1) | ((txt[i] == '1') ? 1 : 0);
}
return ret;
}
/// <summary>
/// Converts a binary string to a byte. Can return null.
/// </summary>
/// <param name="txt"></param>
/// <returns></returns>
public static byte? BinStringToByte(this string txt)
{
if (txt.Length == 8) return (byte)BinStringToInt32(txt);
return null;
}
/// <summary>
/// Converts the value to a binary string
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static string ValToBinString(this object value)
{
int cnt = 0;
int cnt2 = 0;
int x = 0;
string txt = "";
long longValue = 0;
try
{
if (value.GetType().Name.IndexOf("[]") < 0)
{
// ist nur ein Wert
switch (value.GetType().Name)
{
case "Byte":
x = 7;
longValue = (long)((byte)value);
break;
case "Int16":
x = 15;
longValue = (long)((Int16)value);
break;
case "Int32":
x = 31;
longValue = (long)((Int32)value);
break;
case "Int64":
x = 63;
longValue = (long)((Int64)value);
break;
default:
throw new Exception();
}
for (cnt = x; cnt >= 0; cnt += -1)
{
if (((Int64)longValue & (Int64)Math.Pow(2, cnt)) > 0)
txt += "1";
else
txt += "0";
}
}
else
{
// ist ein Array
switch (value.GetType().Name)
{
case "Byte[]":
x = 7;
byte[] ByteArr = (byte[])value;
for (cnt2 = 0; cnt2 <= ByteArr.Length - 1; cnt2++)
{
for (cnt = x; cnt >= 0; cnt += -1)
if ((ByteArr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0";
}
break;
case "Int16[]":
x = 15;
Int16[] Int16Arr = (Int16[])value;
for (cnt2 = 0; cnt2 <= Int16Arr.Length - 1; cnt2++)
{
for (cnt = x; cnt >= 0; cnt += -1)
if ((Int16Arr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0";
}
break;
case "Int32[]":
x = 31;
Int32[] Int32Arr = (Int32[])value;
for (cnt2 = 0; cnt2 <= Int32Arr.Length - 1; cnt2++)
{
for (cnt = x; cnt >= 0; cnt += -1)
if ((Int32Arr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0";
}
break;
case "Int64[]":
x = 63;
byte[] Int64Arr = (byte[])value;
for (cnt2 = 0; cnt2 <= Int64Arr.Length - 1; cnt2++)
{
for (cnt = x; cnt >= 0; cnt += -1)
if ((Int64Arr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0";
}
break;
default:
throw new Exception();
}
}
return txt;
}
catch
{
return "";
}
}
/// <summary>
/// Helper to get a bit value given a byte and the bit index.
/// Example: DB1.DBX0.5 -> var bytes = ReadBytes(DB1.DBW0); bool bit = bytes[0].SelectBit(5);
/// </summary>
/// <param name="data"></param>
/// <param name="bitPosition"></param>
/// <returns></returns>
public static bool SelectBit(this byte data, int bitPosition)
{
int mask = 1 << bitPosition;
int result = data & mask;
return (result != 0);
}
/// <summary>
/// Converts from ushort value to short value; it's used to retrieve negative values from words
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static short ConvertToShort(this ushort input)
{
short output;
output = short.Parse(input.ToString("X"), NumberStyles.HexNumber);
return output;
}
/// <summary>
/// Converts from short value to ushort value; it's used to pass negative values to DWs
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static ushort ConvertToUshort(this short input)
{
ushort output;
output = ushort.Parse(input.ToString("X"), NumberStyles.HexNumber);
return output;
}
/// <summary>
/// Converts from UInt32 value to Int32 value; it's used to retrieve negative values from DBDs
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static Int32 ConvertToInt(this uint input)
{
int output;
output = int.Parse(input.ToString("X"), NumberStyles.HexNumber);
return output;
}
/// <summary>
/// Converts from Int32 value to UInt32 value; it's used to pass negative values to DBDs
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static UInt32 ConvertToUInt(this int input)
{
uint output;
output = uint.Parse(input.ToString("X"), NumberStyles.HexNumber);
return output;
}
/// <summary>
/// Converts from float to DWord (DBD)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static UInt32 ConvertToUInt(this float input)
{
uint output;
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Real.ToByteArray(input));
return output;
}
/// <summary>
/// Converts from DWord (DBD) to float
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static float ConvertToFloat(this uint input)
{
float output;
output = S7.Net.Types.Real.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
return output;
}
}
}

View File

@@ -0,0 +1,211 @@
namespace S7.Net
{
/// <summary>
/// Types of S7 cpu supported by the library
/// </summary>
public enum CpuType
{
/// <summary>
/// S7 200 cpu type
/// </summary>
S7200 = 0,
/// <summary>
/// Siemens Logo 0BA8
/// </summary>
Logo0BA8 = 1,
/// <summary>
/// S7 200 Smart
/// </summary>
S7200Smart = 2,
/// <summary>
/// S7 300 cpu type
/// </summary>
S7300 = 10,
/// <summary>
/// S7 400 cpu type
/// </summary>
S7400 = 20,
/// <summary>
/// S7 1200 cpu type
/// </summary>
S71200 = 30,
/// <summary>
/// S7 1500 cpu type
/// </summary>
S71500 = 40,
}
/// <summary>
/// Types of error code that can be set after a function is called
/// </summary>
public enum ErrorCode
{
/// <summary>
/// The function has been executed correctly
/// </summary>
NoError = 0,
/// <summary>
/// Wrong type of CPU error
/// </summary>
WrongCPU_Type = 1,
/// <summary>
/// Connection error
/// </summary>
ConnectionError = 2,
/// <summary>
/// Ip address not available
/// </summary>
IPAddressNotAvailable,
/// <summary>
/// Wrong format of the variable
/// </summary>
WrongVarFormat = 10,
/// <summary>
/// Wrong number of received bytes
/// </summary>
WrongNumberReceivedBytes = 11,
/// <summary>
/// Error on send data
/// </summary>
SendData = 20,
/// <summary>
/// Error on read data
/// </summary>
ReadData = 30,
/// <summary>
/// Error on write data
/// </summary>
WriteData = 50
}
/// <summary>
/// Types of memory area that can be read
/// </summary>
public enum DataType
{
/// <summary>
/// Input area memory
/// </summary>
Input = 129,
/// <summary>
/// Output area memory
/// </summary>
Output = 130,
/// <summary>
/// Merkers area memory (M0, M0.0, ...)
/// </summary>
Memory = 131,
/// <summary>
/// DB area memory (DB1, DB2, ...)
/// </summary>
DataBlock = 132,
/// <summary>
/// Timer area memory(T1, T2, ...)
/// </summary>
Timer = 29,
/// <summary>
/// Counter area memory (C1, C2, ...)
/// </summary>
Counter = 28
}
/// <summary>
/// Types
/// </summary>
public enum VarType
{
/// <summary>
/// S7 Bit variable type (bool)
/// </summary>
Bit,
/// <summary>
/// S7 Byte variable type (8 bits)
/// </summary>
Byte,
/// <summary>
/// S7 Word variable type (16 bits, 2 bytes)
/// </summary>
Word,
/// <summary>
/// S7 DWord variable type (32 bits, 4 bytes)
/// </summary>
DWord,
/// <summary>
/// S7 Int variable type (16 bits, 2 bytes)
/// </summary>
Int,
/// <summary>
/// DInt variable type (32 bits, 4 bytes)
/// </summary>
DInt,
/// <summary>
/// Real variable type (32 bits, 4 bytes)
/// </summary>
Real,
/// <summary>
/// LReal variable type (64 bits, 8 bytes)
/// </summary>
LReal,
/// <summary>
/// Char Array / C-String variable type (variable)
/// </summary>
String,
/// <summary>
/// S7 String variable type (variable)
/// </summary>
S7String,
/// <summary>
/// S7 WString variable type (variable)
/// </summary>
S7WString,
/// <summary>
/// Timer variable type
/// </summary>
Timer,
/// <summary>
/// Counter variable type
/// </summary>
Counter,
/// <summary>
/// DateTIme variable type
/// </summary>
DateTime,
/// <summary>
/// DateTimeLong variable type
/// </summary>
DateTimeLong
}
}

View File

@@ -0,0 +1,35 @@
using System.IO;
namespace S7.Net.Helper
{
internal static class MemoryStreamExtension
{
/// <summary>
/// Helper function to write to whole content of the given byte array to a memory stream.
///
/// Writes all bytes in value from 0 to value.Length to the memory stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="value"></param>
public static void Write(this MemoryStream stream, byte[] value)
{
stream.Write(value, 0, value.Length);
}
/// <summary>
/// Helper function to write the whole content of the given byte span to a memory stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="value"></param>
//public static void Write(this MemoryStream stream, ReadOnlySpan<byte> value)
//{
// byte[] buffer = ArrayPool<byte>.Shared.Rent(value.Length);
// value.CopyTo(buffer);
// stream.Write(buffer, 0, value.Length);
// ArrayPool<byte>.Shared.Return(buffer);
//}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net.Internal
{
internal class TaskQueue
{
private static readonly object Sentinel = new object();
private Task prev = Task.FromResult(Sentinel);
public async Task<T> Enqueue<T>(Func<Task<T>> action)
{
var tcs = new TaskCompletionSource<object>();
await Interlocked.Exchange(ref prev, tcs.Task).ConfigureAwait(false);
try
{
return await action.Invoke().ConfigureAwait(false);
}
finally
{
tcs.SetResult(Sentinel);
}
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
namespace S7.Net
{
[Serializable]
public class InvalidDataException : Exception
{
public byte[] ReceivedData { get; }
public int ErrorIndex { get; }
public byte ExpectedValue { get; }
public InvalidDataException(string message, byte[] receivedData, int errorIndex, byte expectedValue)
: base(FormatMessage(message, receivedData, errorIndex, expectedValue))
{
ReceivedData = receivedData;
ErrorIndex = errorIndex;
ExpectedValue = expectedValue;
}
protected InvalidDataException(System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
ReceivedData = (byte[])info.GetValue(nameof(ReceivedData), typeof(byte[]));
ErrorIndex = info.GetInt32(nameof(ErrorIndex));
ExpectedValue = info.GetByte(nameof(ExpectedValue));
}
private static string FormatMessage(string message, byte[] receivedData, int errorIndex, byte expectedValue)
{
if (errorIndex >= receivedData.Length)
throw new ArgumentOutOfRangeException(nameof(errorIndex),
$"{nameof(errorIndex)} {errorIndex} is outside the bounds of {nameof(receivedData)} with length {receivedData.Length}.");
return $"{message} Invalid data received. Expected '{expectedValue}' at index {errorIndex}, " +
$"but received {receivedData[errorIndex]}. See the {nameof(ReceivedData)} property " +
"for the full message received.";
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using S7.Net;
namespace MES.Utility.Network.S7netplus.ORM
{
public class S7Client : IDisposable
{
public S7Helper Plc;
public bool IsConnected;
public S7Client(CpuType cpu, string ip)
{
try
{
Plc = new S7Helper(cpu, ip);
IsConnected = true;
}
catch (Exception ex)
{
IsConnected = false;
}
}
public S7Read<T> Readable<T>() where T : class, new()
{
return new S7Read<T>(this);
}
public S7Write<T> Writeable<T>(T entity) where T : class, new()
{
return new S7Write<T>(this, entity);
}
public void Dispose()
{
try
{
if (Plc != null)
{
Plc.Dispose();
Plc = null;
}
}
catch { }
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MES.Utility.Network.S7netplus.ORM
{
public enum S7NodeType
{
[Description("可读可写")]
ReadAndWrite,
[Description("只读")]
ReadOnly,
[Description("只写")]
WriteOnly
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MES.Utility.Network.S7netplus.ORM;
namespace MES.Utility.Network.S7netplus.ORM
{
public class S7PLCAttribute : Attribute
{
public string Address { get; set; }
public int StrLength { get; set; }
public S7NodeType Type { get; set; }
public S7PLCAttribute() { }
public S7PLCAttribute(string address, S7NodeType type)
{
this.Address = address;
this.Type = type;
}
public S7PLCAttribute(string address, int strLength, S7NodeType type)
{
this.Address = address;
this.StrLength = strLength;
this.Type = type;
}
}
}

View File

@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace MES.Utility.Network.S7netplus.ORM
{
public class S7Read<T> where T : class, new()
{
private S7Client opcClient;
private Dictionary<string, PropertyInfo> fieldDict;
private Dictionary<string, int> stringLengthDict;
/// <summary>
/// 构造方法,传入参数
/// </summary>
/// <param name="opcClient"></param>
public S7Read(S7Client opcClient)
{
this.opcClient = opcClient;
//默认取出所有的只读或者可读可写的属性
GetNodeType(out fieldDict, out stringLengthDict);
}
/// <summary>
/// 获得所有具有可读属性的数据
/// </summary>
/// <returns></returns>
private void GetNodeType(out Dictionary<string, PropertyInfo> fieldDict, out Dictionary<string, int> stringLengthDict)
{
fieldDict = new Dictionary<string, PropertyInfo>();
stringLengthDict = new Dictionary<string, int>();
//获得对象T的属性
PropertyInfo[] properties = typeof(T).GetProperties();
//得到所有添加注解的属性
foreach (PropertyInfo info in properties)
{
//判断是否具有HttpController属性
Object[] attr = info.GetCustomAttributes(false);
if (attr.Length == 0)
{
continue;
}
//从注解数组中取第一个注解(一个属性可以包含多个注解)
S7PLCAttribute myattr = attr[0] as S7PLCAttribute;
if (myattr == null)
continue;
if (myattr.Type == S7NodeType.ReadAndWrite || myattr.Type == S7NodeType.ReadOnly)
{
fieldDict.Add(myattr.Address, info);
stringLengthDict.Add(myattr.Address, myattr.StrLength);
}
}
}
/// <summary>
/// 执行获得数据,映射成对象
/// </summary>
/// <param name="message">返回错误信息</param>
/// <returns></returns>
public bool Execute(out T t)
{
if (fieldDict.Count == 0)
{
t = default(T);
return false;
}
try
{
if (!opcClient.IsConnected)
{
t = default(T);
return false;
}
bool flag = false;
T entity = (T)Activator.CreateInstance(typeof(T));
//循环读取每一个
foreach (KeyValuePair<string, PropertyInfo> keyValue in fieldDict)
{
//判断当前keyValue是什么类型
if (keyValue.Value.PropertyType == typeof(ushort))
{
ushort value=0;
flag = opcClient.Plc.ReadInt16(keyValue.Key, out value);
if (!flag)
{
t = default(T);
return false;
}
keyValue.Value.SetValue(entity, value);
}
else if (keyValue.Value.PropertyType == typeof(int))
{
ushort value =0;
flag = opcClient.Plc.ReadInt16(keyValue.Key, out value);
if (!flag)
{
t = default(T);
return false;
}
keyValue.Value.SetValue(entity, value);
}
//else if (keyValue.Value.PropertyType == typeof(float))
//{
// float value;
// flag = opcClient.Plc.ReadReal(keyValue.Key, out value);
// if (!flag)
// {
// t = default(T);
// return false;
// }
// keyValue.Value.SetValue(entity, value);
//}
else if (keyValue.Value.PropertyType == typeof(string))
{
string value;
flag = opcClient.Plc.ReadString(keyValue.Key, stringLengthDict[keyValue.Key], out value);
if (!flag)
{
t = default(T);
return false;
}
keyValue.Value.SetValue(entity, value);
}
}
t = entity;
return true;
}
catch
{
t = default(T);
return false;
}
}
/// <summary>
/// 传入Lambda表达式指定读取的数据
/// </summary>
/// <param name="columns"></param>
/// <returns></returns>
public S7Read<T> ReadField(Expression<Func<T, object>> columns)
{
Dictionary<string, PropertyInfo> fieldDict = new Dictionary<string, PropertyInfo>();
Dictionary<string, int> stringLengthDict = new Dictionary<string, int>();
//取出要读的属性
string lambda = columns.Body.ToString();
int index1 = lambda.IndexOf('(');
int index2 = lambda.IndexOf(')');
string str = lambda.Substring(index1 + 1, index2 - index1 - 1);
List<string> keyList = str.Split(',').Select(it => it.Split('=')[0].Trim()).ToList();
fieldDict.Clear();
//获得对象T的属性
PropertyInfo[] properties = typeof(T).GetProperties();
//得到所有添加注解的属性
foreach (PropertyInfo info in properties)
{
//判断是否具有HttpController属性
Object[] attr = info.GetCustomAttributes(false);
if (attr.Length == 0)
{
continue;
}
//从注解数组中取第一个注解(一个属性可以包含多个注解)
S7PLCAttribute myattr = attr[0] as S7PLCAttribute;
if (myattr == null)
continue;
if (myattr.Type == S7NodeType.ReadAndWrite || myattr.Type == S7NodeType.ReadOnly)
{
if (keyList.Contains(info.Name))
{
fieldDict.Add(myattr.Address, info);
stringLengthDict.Add(myattr.Address, myattr.StrLength);
}
}
}
this.fieldDict = fieldDict;
this.stringLengthDict = stringLengthDict;
return this;
}
}
}

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace MES.Utility.Network.S7netplus.ORM
{
public class S7Write<T> where T : class, new()
{
private S7Client opcClient;
private T entity;
private Dictionary<string, PropertyInfo> dict;
private Dictionary<string, int> strLengthDict;
/// <summary>
/// 构造方法,传入参数
/// </summary>
/// <param name="opcClient"></param>
/// <param name="entity"></param>
public S7Write(S7Client opcClient, T entity)
{
this.opcClient = opcClient;
this.entity = entity;
//默认取出所有的只写或者可读可写的属性
GetAllKeyList(out dict, out strLengthDict);
}
/// <summary>
/// 获得所有具有可写属性的数据
/// </summary>
/// <returns></returns>
private void GetAllKeyList(out Dictionary<string, PropertyInfo> dict, out Dictionary<string, int> strLengthDict)
{
dict = new Dictionary<string, PropertyInfo>();
strLengthDict = new Dictionary<string, int>();
//获取entity对象的所有属性
PropertyInfo[] properties = typeof(T).GetProperties();
//得到所有添加注解的属性
foreach (PropertyInfo info in properties)
{
//判断是否具有HttpController属性
object[] attr = info.GetCustomAttributes(false);
if (attr.Length == 0)
{
continue;
}
//从注解数组中取第一个注解(一个属性可以包含多个注解)
S7PLCAttribute myattr = attr[0] as S7PLCAttribute;
if (myattr == null)
continue;
if (myattr.Type == S7NodeType.ReadAndWrite || myattr.Type == S7NodeType.WriteOnly)
{
dict.Add(myattr.Address, info);
strLengthDict.Add(myattr.Address, myattr.StrLength);
}
}
}
/// <summary>
/// 执行写入数据将对象中的数据写入到OPC服务中
/// </summary>
/// <param name="message">返回写入过程中出现的错误信息</param>
/// <returns></returns>
public bool Execute()
{
if (dict.Count == 0)
{
return false;
}
if (!opcClient.IsConnected)
return false;
//循环写入
try
{
bool flag;
foreach (KeyValuePair<string, PropertyInfo> keyValue in dict)
{
//判断当前keyValue是什么类型
if (keyValue.Value.PropertyType == typeof(ushort))
{
flag = opcClient.Plc.WriteInt16(keyValue.Key, (ushort)keyValue.Value.GetValue(entity));
if (!flag)
{
return false;
}
}
else if (keyValue.Value.PropertyType == typeof(int))
{
flag = opcClient.Plc.WriteInt32(keyValue.Key, (int)keyValue.Value.GetValue(entity));
if (!flag)
{
return false;
}
}
else if (keyValue.Value.PropertyType == typeof(float))
{
flag = opcClient.Plc.WriteSingle(keyValue.Key, (float)keyValue.Value.GetValue(entity));
if (!flag)
{
return false;
}
}
else if (keyValue.Value.PropertyType == typeof(string))
{
flag = opcClient.Plc.WriteString(keyValue.Key, (string)keyValue.Value.GetValue(entity), strLengthDict[keyValue.Key]);
if (!flag)
{
return false;
}
}
}
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 传入Lambda表达式指定写入的数据
/// </summary>
/// <param name="columns"></param>
/// <returns></returns>
public S7Write<T> WriteField(Expression<Func<T, object>> columns)
{
Dictionary<string, PropertyInfo> dict = new Dictionary<string, PropertyInfo>();
Dictionary<string, int> strLengthDict = new Dictionary<string, int>();
string lambda = columns.Body.ToString();
int index1 = lambda.IndexOf('(');
int index2 = lambda.IndexOf(')');
string str = lambda.Substring(index1 + 1, index2 - index1 - 1);
List<string> keyList = str.Split(',').Select(it => it.Split('=')[0].Trim()).ToList();
//获取entity对象的所有属性
PropertyInfo[] properties = typeof(T).GetProperties();
//得到所有添加注解的属性
foreach (PropertyInfo info in properties)
{
//判断是否具有HttpController属性
Object[] attr = info.GetCustomAttributes(false);
if (attr.Length == 0)
{
continue;
}
//从注解数组中取第一个注解(一个属性可以包含多个注解)
S7PLCAttribute myattr = attr[0] as S7PLCAttribute;
if (myattr == null)
continue;
if (myattr.Type == S7NodeType.ReadAndWrite || myattr.Type == S7NodeType.WriteOnly)
{
string name = info.Name;
if (keyList.Contains(name))
{
dict.Add(myattr.Address, info);
strLengthDict.Add(myattr.Address, myattr.StrLength);
}
}
}
this.dict = dict;
this.strLengthDict = strLengthDict;
return this;
}
}
}

View File

@@ -0,0 +1,329 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using S7.Net.Internal;
using S7.Net.Protocol;
using S7.Net.Types;
namespace S7.Net
{
/// <summary>
/// Creates an instance of S7.Net driver
/// </summary>
public partial class Plc : IDisposable
{
/// <summary>
/// The default port for the S7 protocol.
/// </summary>
public const int DefaultPort = 102;
/// <summary>
/// The default timeout (in milliseconds) used for <see cref="P:ReadTimeout"/> and <see cref="P:WriteTimeout"/>.
/// </summary>
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
/// <summary>
/// IP address of the PLC
/// </summary>
public string IP { get; }
/// <summary>
/// PORT Number of the PLC, default is 102
/// </summary>
public int Port { get; }
/// <summary>
/// The TSAP addresses used during the connection request.
/// </summary>
public TsapPair TsapPair { get; set; }
/// <summary>
/// CPU type of the PLC
/// </summary>
public CpuType CPU { get; }
/// <summary>
/// Rack of the PLC
/// </summary>
public Int16 Rack { get; }
/// <summary>
/// Slot of the CPU of the PLC
/// </summary>
public Int16 Slot { get; }
/// <summary>
/// Max PDU size this cpu supports
/// </summary>
public int MaxPDUSize { get; private set; }
/// <summary>Gets or sets the amount of time that a read operation blocks waiting for data from PLC.</summary>
/// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a read operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the read operation does not time out.</returns>
public int ReadTimeout
{
get => readTimeout;
set
{
readTimeout = value;
if (tcpClient != null) tcpClient.ReceiveTimeout = readTimeout;
}
}
/// <summary>Gets or sets the amount of time that a write operation blocks waiting for data to PLC. </summary>
/// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a write operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the write operation does not time out.</returns>
public int WriteTimeout
{
get => writeTimeout;
set
{
writeTimeout = value;
if (tcpClient != null) tcpClient.SendTimeout = writeTimeout;
}
}
/// <summary>
/// Gets a value indicating whether a connection to the PLC has been established.
/// </summary>
/// <remarks>
/// The <see cref="IsConnected"/> property gets the connection state of the Client socket as
/// of the last I/O operation. When it returns <c>false</c>, the Client socket was either
/// never connected, or is no longer connected.
///
/// <para>
/// Because the <see cref="IsConnected"/> 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 <c>true</c>. 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.
/// </para>
/// </remarks>
public bool IsConnected => tcpClient?.Connected ?? false;
/// <summary>
/// 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.
/// </summary>
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
/// <param name="slot">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.</param>
public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
: this(cpu, ip, DefaultPort, rack, slot)
{
}
/// <summary>
/// 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.
/// </summary>
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="port">Port number used for the connection, default 102.</param>
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
/// <param name="slot">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.</param>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair)
{
}
/// <summary>
/// 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.
/// </summary>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="port">Port number used for the connection, default 102.</param>
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
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;
}
/// <summary>
/// Close connection to PLC
/// </summary>
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<DataItem> 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<DataItem> 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<DataItem> 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}.");
}
}
private Stream GetStreamIfAvailable()
{
if (_stream == null)
{
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
}
return _stream;
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
/// <summary>
/// Dispose Plc Object
/// </summary>
/// <param name="disposing"></param>
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
}
}

View File

@@ -0,0 +1,207 @@
namespace S7.Net
{
internal class PLCAddress
{
private DataType dataType;
private int dbNumber;
private int startByte;
private int bitNumber;
private VarType varType;
public DataType DataType
{
get => dataType;
set => dataType = value;
}
public int DbNumber
{
get => dbNumber;
set => dbNumber = value;
}
public int StartByte
{
get => startByte;
set => startByte = value;
}
public int BitNumber
{
get => bitNumber;
set => bitNumber = value;
}
public VarType VarType
{
get => varType;
set => varType = value;
}
public PLCAddress(string address)
{
Parse(address, out dataType, out dbNumber, out varType, out startByte, out bitNumber);
}
public static void Parse(string input, out DataType dataType, out int dbNumber, out VarType varType, out int address, out int bitNumber)
{
bitNumber = -1;
dbNumber = 0;
switch (input.Substring(0, 2))
{
case "DB":
string[] strings = input.Split(new char[] { '.' });
if (strings.Length < 2)
throw new InvalidAddressException("To few periods for DB address");
dataType = DataType.DataBlock;
dbNumber = int.Parse(strings[0].Substring(2));
address = int.Parse(strings[1].Substring(3));
string dbType = strings[1].Substring(0, 3);
switch (dbType)
{
case "DBB":
varType = VarType.Byte;
return;
case "DBW":
varType = VarType.Word;
return;
case "DBD":
varType = VarType.DWord;
return;
case "DBX":
bitNumber = int.Parse(strings[2]);
if (bitNumber > 7)
throw new InvalidAddressException("Bit can only be 0-7");
varType = VarType.Bit;
return;
default:
throw new InvalidAddressException();
}
case "IB":
case "EB":
// Input byte
dataType = DataType.Input;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "IW":
case "EW":
// Input word
dataType = DataType.Input;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "ID":
case "ED":
// Input double-word
dataType = DataType.Input;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
case "QB":
case "AB":
case "OB":
// Output byte
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "QW":
case "AW":
case "OW":
// Output word
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "QD":
case "AD":
case "OD":
// Output double-word
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
case "MB":
// Memory byte
dataType = DataType.Memory;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "MW":
// Memory word
dataType = DataType.Memory;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "MD":
// Memory double-word
dataType = DataType.Memory;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
default:
switch (input.Substring(0, 1))
{
case "E":
case "I":
// Input
dataType = DataType.Input;
varType = VarType.Bit;
break;
case "Q":
case "A":
case "O":
// Output
dataType = DataType.Output;
varType = VarType.Bit;
break;
case "M":
// Memory
dataType = DataType.Memory;
varType = VarType.Bit;
break;
case "T":
// Timer
dataType = DataType.Timer;
dbNumber = 0;
address = int.Parse(input.Substring(1));
varType = VarType.Timer;
return;
case "Z":
case "C":
// Counter
dataType = DataType.Counter;
dbNumber = 0;
address = int.Parse(input.Substring(1));
varType = VarType.Counter;
return;
default:
throw new InvalidAddressException(string.Format("{0} is not a valid address", input.Substring(0, 1)));
}
string txt2 = input.Substring(1);
if (txt2.IndexOf(".") == -1)
throw new InvalidAddressException("To few periods for DB address");
address = int.Parse(txt2.Substring(0, txt2.IndexOf(".")));
bitNumber = int.Parse(txt2.Substring(txt2.IndexOf(".") + 1));
if (bitNumber > 7)
throw new InvalidAddressException("Bit can only be 0-7");
return;
}
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Runtime.Serialization;
namespace S7.Net
{
public class WrongNumberOfBytesException : Exception
{
public WrongNumberOfBytesException() : base()
{
}
public WrongNumberOfBytesException(string message) : base(message)
{
}
public WrongNumberOfBytesException(string message, Exception innerException) : base(message, innerException)
{
}
protected WrongNumberOfBytesException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
public class InvalidAddressException : Exception
{
public InvalidAddressException() : base ()
{
}
public InvalidAddressException(string message) : base(message)
{
}
public InvalidAddressException(string message, Exception innerException) : base(message, innerException)
{
}
protected InvalidAddressException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
public class InvalidVariableTypeException : Exception
{
public InvalidVariableTypeException() : base()
{
}
public InvalidVariableTypeException(string message) : base(message)
{
}
public InvalidVariableTypeException(string message, Exception innerException) : base(message, innerException)
{
}
protected InvalidVariableTypeException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
public class TPKTInvalidException : Exception
{
public TPKTInvalidException() : base()
{
}
public TPKTInvalidException(string message) : base(message)
{
}
public TPKTInvalidException(string message, Exception innerException) : base(message, innerException)
{
}
protected TPKTInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
public class TPDUInvalidException : Exception
{
public TPDUInvalidException() : base()
{
}
public TPDUInvalidException(string message) : base(message)
{
}
public TPDUInvalidException(string message, Exception innerException) : base(message, innerException)
{
}
protected TPDUInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -0,0 +1,265 @@
using S7.Net.Helper;
using S7.Net.Protocol.S7;
using S7.Net.Types;
using System.Collections.Generic;
using System.Linq;
using DateTime = S7.Net.Types.DateTime;
namespace S7.Net
{
public partial class Plc
{
/// <summary>
/// Creates the header to read bytes from the PLC
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1)
{
//header size = 19 bytes
stream.Write(new byte[] { 0x03, 0x00 });
//complete package size
stream.Write(Int.ToByteArray((short)(19 + (12 * amount))));
stream.Write(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 });
//data part size
stream.Write(Word.ToByteArray((ushort)(2 + (amount * 12))));
stream.Write(new byte[] { 0x00, 0x00, 0x04 });
//amount of requests
stream.WriteByte((byte)amount);
}
/// <summary>
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
/// the address of the memory, the address of the byte and the bytes count.
/// </summary>
/// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
/// <param name="db">Address of the memory to be read</param>
/// <param name="startByteAdr">Start address of the byte</param>
/// <param name="count">Number of bytes to be read</param>
/// <returns></returns>
private static void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1)
{
//single data req = 12
stream.Write(new byte[] { 0x12, 0x0a, 0x10 });
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
stream.WriteByte((byte)dataType);
break;
default:
stream.WriteByte(0x02);
break;
}
stream.Write(Word.ToByteArray((ushort)(count)));
stream.Write(Word.ToByteArray((ushort)(db)));
stream.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
stream.WriteByte((byte)overflow);
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
stream.Write(Word.ToByteArray((ushort)(startByteAdr)));
break;
default:
stream.Write(Word.ToByteArray((ushort)((startByteAdr) * 8)));
break;
}
}
/// <summary>
/// Given a S7 variable type (Bool, Word, DWord, etc.), it converts the bytes in the appropriate C# format.
/// </summary>
/// <param name="varType"></param>
/// <param name="bytes"></param>
/// <param name="varCount"></param>
/// <param name="bitAdr"></param>
/// <returns></returns>
private object ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0)
{
if (bytes == null || bytes.Length == 0)
return null;
switch (varType)
{
case VarType.Byte:
if (varCount == 1)
return bytes[0];
else
return bytes;
case VarType.Word:
if (varCount == 1)
return Word.FromByteArray(bytes);
else
return Word.ToArray(bytes);
case VarType.Int:
if (varCount == 1)
return Int.FromByteArray(bytes);
else
return Int.ToArray(bytes);
case VarType.DWord:
if (varCount == 1)
return DWord.FromByteArray(bytes);
else
return DWord.ToArray(bytes);
case VarType.DInt:
if (varCount == 1)
return DInt.FromByteArray(bytes);
else
return DInt.ToArray(bytes);
case VarType.Real:
if (varCount == 1)
return Types.Real.FromByteArray(bytes);
else
return Types.Real.ToArray(bytes);
case VarType.LReal:
if (varCount == 1)
return Types.LReal.FromByteArray(bytes);
else
return Types.LReal.ToArray(bytes);
case VarType.String:
return Types.String.FromByteArray(bytes);
case VarType.S7String:
return S7String.FromByteArray(bytes);
case VarType.S7WString:
return S7WString.FromByteArray(bytes);
case VarType.Timer:
if (varCount == 1)
return Timer.FromByteArray(bytes);
else
return Timer.ToArray(bytes);
case VarType.Counter:
if (varCount == 1)
return Counter.FromByteArray(bytes);
else
return Counter.ToArray(bytes);
case VarType.Bit:
if (varCount == 1)
{
if (bitAdr > 7)
return null;
else
return Bit.FromByte(bytes[0], bitAdr);
}
else
{
return Bit.ToBitArray(bytes, varCount);
}
case VarType.DateTime:
if (varCount == 1)
{
return DateTime.FromByteArray(bytes);
}
else
{
return DateTime.ToArray(bytes);
}
case VarType.DateTimeLong:
if (varCount == 1)
{
return DateTimeLong.FromByteArray(bytes);
}
else
{
return DateTimeLong.ToArray(bytes);
}
default:
return null;
}
}
/// <summary>
/// Given a S7 <see cref="VarType"/> (Bool, Word, DWord, etc.), it returns how many bytes to read.
/// </summary>
/// <param name="varType"></param>
/// <param name="varCount"></param>
/// <returns>Byte lenght of variable</returns>
internal static int VarTypeToByteLength(VarType varType, int varCount = 1)
{
switch (varType)
{
case VarType.Bit:
return (varCount + 7) / 8;
case VarType.Byte:
return (varCount < 1) ? 1 : varCount;
case VarType.String:
return varCount;
case VarType.S7String:
return ((varCount + 2) & 1) == 1 ? (varCount + 3) : (varCount + 2);
case VarType.S7WString:
return (varCount * 2) + 4;
case VarType.Word:
case VarType.Timer:
case VarType.Int:
case VarType.Counter:
return varCount * 2;
case VarType.DWord:
case VarType.DInt:
case VarType.Real:
return varCount * 4;
case VarType.LReal:
case VarType.DateTime:
return varCount * 8;
case VarType.DateTimeLong:
return varCount * 12;
default:
return 0;
}
}
private byte[] GetS7ConnectionSetup()
{
return new byte[] { 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3,
3, 192 // Use 960 PDU size
};
}
private void ParseDataIntoDataItems(byte[] s7data, List<DataItem> dataItems)
{
int offset = 14;
foreach (var dataItem in dataItems)
{
// check for Return Code = Success
if (s7data[offset] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
// to Data bytes
offset += 4;
int byteCnt = VarTypeToByteLength(dataItem.VarType, dataItem.Count);
dataItem.Value = ParseBytes(
dataItem.VarType,
s7data.Skip(offset).Take(byteCnt).ToArray(),
dataItem.Count,
dataItem.BitAdr
);
// next Item
offset += byteCnt;
// Always align to even offset
if (offset % 2 != 0)
offset++;
}
}
private static byte[] BuildReadRequestPackage(IList<DataItemAddress> dataItems)
{
int packageSize = 19 + (dataItems.Count * 12);
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package, dataItems.Count);
foreach (var dataItem in dataItems)
{
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAddress, dataItem.ByteLength);
}
return package.ToArray();
}
}
}

View File

@@ -0,0 +1,582 @@
using S7.Net.Types;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
using S7.Net.Protocol;
using System.Threading;
using S7.Net.Protocol.S7;
namespace S7.Net
{
/// <summary>
/// Creates an instance of S7.Net driver
/// </summary>
public partial class Plc
{
/// <summary>
/// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that the cancellation will not affect opening the socket in any way and only affects data transfers for configuring the connection after the socket connection is successfully established.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous open operation.</returns>
public async Task OpenAsync(CancellationToken cancellationToken = default)
{
var stream = await ConnectAsync(cancellationToken).ConfigureAwait(false);
try
{
await queue.Enqueue(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
return default(object);
}).ConfigureAwait(false);
}
catch (Exception)
{
stream.Dispose();
throw;
}
}
private async Task<NetworkStream> ConnectAsync(CancellationToken cancellationToken)
{
tcpClient = new TcpClient();
ConfigureConnection();
#if NETFRAMEWORK
IAsyncResult asyncResult = tcpClient.BeginConnect(IP, Port, null, null);
if (!asyncResult.AsyncWaitHandle.WaitOne(1000))
{
throw new Exception("<22><><EFBFBD>ӳ<EFBFBD>ʱ");
}
tcpClient.EndConnect(asyncResult);
#else
await tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false);
#endif
return tcpClient.GetStream();
}
private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken)
{
await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
}
private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
{
var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair);
var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
{
throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d);
}
}
private async Task SetupConnection(Stream stream, CancellationToken cancellationToken)
{
var setupData = GetS7ConnectionSetup();
var s7data = await NoLockRequestTsduAsync(stream, setupData, 0, setupData.Length, cancellationToken)
.ConfigureAwait(false);
if (s7data.Length < 2)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
//Check for S7 Ack Data
if (s7data[1] != 0x03)
throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
if (s7data.Length < 20)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
// TODO: check if this should not rather be UInt16.
MaxPDUSize = s7data[18] * 256 + s7data[19];
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns the bytes in an array</returns>
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default)
{
var resultBytes = new byte[count];
int index = 0;
while (count > 0)
{
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = Math.Min(count, MaxPDUSize - 18);
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, resultBytes, index, maxToRead, cancellationToken).ConfigureAwait(false);
count -= maxToRead;
index += maxToRead;
}
return resultBytes;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="varType">Type of the variable/s that you are reading</param>
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
/// <param name="varCount"></param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
public async Task<object> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0, CancellationToken cancellationToken = default)
{
int cntBytes = VarTypeToByteLength(varType, varCount);
byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes, cancellationToken).ConfigureAwait(false);
return ParseBytes(varType, bytes, varCount, bitAdr);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
public async Task<object> ReadAsync(string variable, CancellationToken cancellationToken = default)
{
var adr = new PLCAddress(variable);
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns a struct that must be cast.</returns>
public async Task<object> ReadStructAsync(Type structType, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
int numBytes = Types.Struct.GetStructSize(structType);
// now read the package
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
// and decode it
return Types.Struct.FromBytes(structType, resultBytes);
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The struct type</typeparam>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns a nulable struct. If nothing was read null will be returned.</returns>
public async Task<T?> ReadStructAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : struct
{
return await ReadStructAsync(typeof(T), db, startByteAdr, cancellationToken).ConfigureAwait(false) as T?;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>The number of read bytes</returns>
public async Task<Tuple<int, object>> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
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 = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
// and decode it
Class.FromBytes(sourceClass, resultBytes);
return new Tuple<int, object>(resultBytes.Length, sourceClass);
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The class that will be instantiated. Requires a default constructor</typeparam>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public async Task<T> ReadClassAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
{
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam>
/// <param name="classFactory">Function to instantiate the class</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public async Task<T> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
{
var instance = classFactory();
var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken).ConfigureAwait(false);
int readBytes = res.Item1;
if (readBytes <= 0)
{
return null;
}
return (T)res.Item2;
}
/// <summary>
/// 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).
/// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems, CancellationToken cancellationToken = default)
{
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
AssertPduSizeForRead(dataItems);
try
{
var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList());
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
}
catch (SocketException socketException)
{
throw new PlcException(ErrorCode.ReadData, socketException);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ReadData, exc);
}
return dataItems;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken = default)
{
int localIndex = 0;
int count = value.Length;
while (count > 0)
{
var maxToWrite = (int)Math.Min(count, MaxPDUSize - 35);
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite, cancellationToken).ConfigureAwait(false);
count -= maxToWrite;
localIndex += maxToWrite;
}
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value, CancellationToken cancellationToken = default)
{
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));
await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value, CancellationToken cancellationToken = default)
{
if (value < 0 || value > 1)
throw new ArgumentException("Value must be 0 or 1", nameof(value));
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1, CancellationToken cancellationToken = default)
{
if (bitAdr != -1)
{
//Must be writing a bit value as bitAdr is specified
if (value is bool boolean)
{
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken).ConfigureAwait(false);
}
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));
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1, cancellationToken).ConfigureAwait(false);
}
else throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
}
else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 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 <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteAsync(string variable, object value, CancellationToken cancellationToken = default)
{
var adr = new PLCAddress(variable);
await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Writes a C# struct to a DB in the PLC
/// </summary>
/// <param name="structValue">The struct to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
var bytes = Struct.ToBytes(structValue).ToList();
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Writes a C# class to a DB in the PLC
/// </summary>
/// <param name="classValue">The class to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteClassAsync(object classValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
byte[] bytes = new byte[(int)Class.GetClassSize(classValue)];
Types.Class.ToBytes(classValue, bytes);
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false);
}
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, count) });
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
AssertReadResponse(s7data, count);
Array.Copy(s7data, 18, buffer, offset, count);
}
/// <summary>
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
/// or when the PLC reports errors for item(s) written.
/// </summary>
/// <param name="dataItems">The DataItem(s) to write to the PLC.</param>
/// <returns>Task that completes when response from PLC is parsed.</returns>
public async Task WriteAsync(params DataItem[] dataItems)
{
AssertPduSizeForWrite(dataItems);
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false);
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
/// <summary>
/// Writes up to 200 bytes to the PLC. You must specify the memory area type, memory are address, byte start address and bytes count.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count, CancellationToken cancellationToken)
{
try
{
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
{
try
{
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
private Task<byte[]> RequestTsduAsync(byte[] requestData, CancellationToken cancellationToken = default) =>
RequestTsduAsync(requestData, 0, requestData.Length, cancellationToken);
private Task<byte[]> RequestTsduAsync(byte[] requestData, int offset, int length, CancellationToken cancellationToken = default)
{
var stream = GetStreamIfAvailable();
return queue.Enqueue(() =>
NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
}
private async Task<COTP.TPDU> NoLockRequestTpduAsync(Stream stream, byte[] requestData,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using (var closeOnCancellation = cancellationToken.Register(Close))
{
await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false);
return await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception exc)
{
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
{
Close();
}
throw;
}
}
private async Task<byte[]> NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using (var closeOnCancellation = cancellationToken.Register(Close))
{
await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false);
return await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception exc)
{
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
{
Close();
}
throw;
}
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
namespace S7.Net
{
[Serializable]
public class PlcException : Exception
{
public ErrorCode ErrorCode { get; }
public PlcException(ErrorCode errorCode) : this(errorCode, $"PLC communication failed with error '{errorCode}'.")
{
}
public PlcException(ErrorCode errorCode, Exception innerException) : this(errorCode, innerException.Message,
innerException)
{
}
public PlcException(ErrorCode errorCode, string message) : base(message)
{
ErrorCode = errorCode;
}
public PlcException(ErrorCode errorCode, string message, Exception inner) : base(message, inner)
{
ErrorCode = errorCode;
}
protected PlcException(System.Runtime.Serialization.SerializationInfo info,System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
ErrorCode = (ErrorCode) info.GetInt32(nameof(ErrorCode));
}
}
}

View File

@@ -0,0 +1,585 @@
using S7.Net.Types;
using System;
using System.IO;
using System.Collections.Generic;
using S7.Net.Protocol;
using S7.Net.Helper;
using System.Net.Sockets;
using System.Threading;
//Implement synchronous methods here
namespace S7.Net
{
public partial class Plc
{
/// <summary>
/// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup.
/// </summary>
public void Open(int timeout = 0)
{
try
{
if (timeout == 0)
{
OpenAsync().GetAwaiter().GetResult();
}
else
{
CancellationTokenSource cts = new CancellationTokenSource(timeout);
OpenAsync(cts.Token).GetAwaiter().GetResult();
}
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ConnectionError,
$"Couldn't establish the connection to {IP}.\nMessage: {exc.Message}", exc);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
/// <returns>Returns the bytes in an array</returns>
public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
{
//var result = new byte[count];
//ReadBytes(result, dataType, db, startByteAdr);
//return result;
var result = new byte[count];
int index = 0;
while (count > 0)
{
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = Math.Min(count, MaxPDUSize - 18);
ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, result, index, maxToRead);
count -= maxToRead;
index += maxToRead;
}
return result;
}
private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count)
{
try
{
// first create the header
int packageSize = 19 + 12; // 19 header + 12 for 1 request
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package);
// package.Add(0x02); // datenart
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count);
var dataToSend = package.ToArray();
var s7data = RequestTsdu(dataToSend);
AssertReadResponse(s7data, count);
Array.Copy(s7data, 18, buffer, offset, count);
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ReadData, exc);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="varType">Type of the variable/s that you are reading</param>
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
/// <param name="varCount"></param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <returns>Returns an object that contains the value. This object must be cast accordingly. If no data has been read, null will be returned</returns>
public object Read(string variable)
{
var adr = new PLCAddress(variable);
return Read(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a struct that must be cast. If no data has been read, null will be returned</returns>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The struct type</typeparam>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a nullable struct. If nothing was read null will be returned.</returns>
public T? ReadStruct<T>(int db, int startByteAdr = 0) where T : struct
{
return ReadStruct(typeof(T), db, startByteAdr) as T?;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>The number of read bytes</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The class that will be instantiated. Requires a default constructor</typeparam>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public T ReadClass<T>(int db, int startByteAdr = 0) where T : class
{
return ReadClass(() => Activator.CreateInstance<T>(), db, startByteAdr);
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam>
/// <param name="classFactory">Function to instantiate the class</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public T ReadClass<T>(Func<T> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value)
{
//WriteBytes(dataType, db, startByteAdr, value.AsSpan());
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 = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite);
count -= maxToWrite;
localIndex += maxToWrite;
}
}
private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count)
{
try
{
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
var s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
//public void WriteBytes(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> 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;
// }
//}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
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);
}
/// <summary>
/// Write a single bit to a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Value to write (0 or 1).</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">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.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
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));
}
/// <summary>
/// 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 <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param>
public void Write(string variable, object value)
{
var adr = new PLCAddress(variable);
Write(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber);
}
/// <summary>
/// Writes a C# struct to a DB in the PLC
/// </summary>
/// <param name="structValue">The struct to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
public void WriteStruct(object structValue, int db, int startByteAdr = 0)
{
WriteStructAsync(structValue, db, startByteAdr).GetAwaiter().GetResult();
}
/// <summary>
/// Writes a C# class to a DB in the PLC
/// </summary>
/// <param name="classValue">The class to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
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<byte> 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);
// BuildHeaderPackage(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);
// }
//}
/// <summary>
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
/// or when the PLC reports errors for item(s) written.
/// </summary>
/// <param name="dataItems">The DataItem(s) to write to the PLC.</param>
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<byte> 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, byte[] value, int dataOffset, int count)
{
int varCount = count;
// first create the header
int packageSize = 35 + varCount;
var package = new MemoryStream(new byte[packageSize]);
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, 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, dataOffset, count);
return package.ToArray();
}
//private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> 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);
}
}
/// <summary>
/// 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).
/// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
public void ReadMultipleVars(List<DataItem> dataItems)
{
AssertPduSizeForRead(dataItems);
try
{
// first create the header
int packageSize = 19 + (dataItems.Count * 12);
var dataToSend = new byte[packageSize];
var package = new MemoryStream(dataToSend);
BuildHeaderPackage(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);
}
}
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();
}
}
}

View File

@@ -0,0 +1,28 @@
namespace S7.Net.Protocol
{
internal static class ConnectionRequest
{
public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair)
{
byte[] bSend1 = {
3, 0, 0, 22, //TPKT
17, //COTP Header Length
224, //Connect Request
0, 0, //Destination Reference
0, 46, //Source Reference
0, //Flags
193, //Parameter Code (src-tasp)
2, //Parameter Length
tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, //Source TASP
194, //Parameter Code (dst-tasp)
2, //Parameter Length
tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, //Destination TASP
192, //Parameter Code (tpdu-size)
1, //Parameter Length
10 //TPDU Size (2^10 = 1024)
};
return bSend1;
}
}
}

View File

@@ -0,0 +1,15 @@

namespace S7.Net.Protocol
{
internal enum ReadWriteErrorCode : byte
{
Reserved = 0x00,
HardwareFault = 0x01,
AccessingObjectNotAllowed = 0x03,
AddressOutOfRange = 0x05,
DataTypeNotSupported = 0x06,
DataTypeInconsistent = 0x07,
ObjectDoesNotExist = 0x0a,
Success = 0xff
}
}

View File

@@ -0,0 +1,37 @@
namespace S7.Net.Protocol.S7
{
/// <summary>
/// Represents an area of memory in the PLC
/// </summary>
internal class DataItemAddress
{
public DataItemAddress(DataType dataType, int db, int startByteAddress, int byteLength)
{
DataType = dataType;
DB = db;
StartByteAddress = startByteAddress;
ByteLength = byteLength;
}
/// <summary>
/// Memory area to read
/// </summary>
public DataType DataType { get; }
/// <summary>
/// Address of memory area to read (example: for DB1 this value is 1, for T45 this value is 45)
/// </summary>
public int DB { get; }
/// <summary>
/// Address of the first byte to read
/// </summary>
public int StartByteAddress { get; }
/// <summary>
/// Length of data to read
/// </summary>
public int ByteLength { get; }
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using S7.Net.Types;
namespace S7.Net.Protocol
{
internal static class S7WriteMultiple
{
public static int CreateRequest(ByteArray message, DataItem[] dataItems)
{
message.Add(Header.Template);
message[Header.Offsets.ParameterCount] = (byte) dataItems.Length;
var paramSize = dataItems.Length * Parameter.Template.Length;
Serialization.SetWordAt(message, Header.Offsets.ParameterSize,
(ushort) (2 + paramSize));
var paramOffset = Header.Template.Length;
var data = new ByteArray();
var itemCount = 0;
foreach (var item in dataItems)
{
itemCount++;
message.Add(Parameter.Template);
var value = Serialization.SerializeDataItem(item);
var wordLen = item.Value is bool ? 1 : 2;
message[paramOffset + Parameter.Offsets.WordLength] = (byte) wordLen;
Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.Amount, (ushort) value.Length);
Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.DbNumber, (ushort) item.DB);
message[paramOffset + Parameter.Offsets.Area] = (byte) item.DataType;
data.Add(0x00);
if (item.Value is bool b)
{
if (item.BitAdr > 7)
throw new ArgumentException(
$"Cannot read bit with invalid {nameof(item.BitAdr)} '{item.BitAdr}'.", nameof(dataItems));
Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr,
item.BitAdr);
data.Add(0x03);
data.AddWord(1);
data.Add(b ? (byte)1 : (byte)0);
if (itemCount != dataItems.Length) {
data.Add(0);
}
}
else
{
Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr, 0);
var len = value.Length;
data.Add(0x04);
data.AddWord((ushort) (len << 3));
data.Add(value);
if ((len & 0b1) == 1 && itemCount != dataItems.Length)
{
data.Add(0);
}
}
paramOffset += Parameter.Template.Length;
}
message.Add(data.Array);
Serialization.SetWordAt(message, Header.Offsets.MessageLength, (ushort) message.Length);
Serialization.SetWordAt(message, Header.Offsets.DataLength, (ushort) (message.Length - paramOffset));
return message.Length;
}
public static void ParseResponse(byte[] message, int length, DataItem[] dataItems)
{
if (length < 12) throw new Exception("Not enough data received to parse write response.");
var messageError = Serialization.GetWordAt(message, 10);
if (messageError != 0)
throw new Exception($"Write failed with error {messageError}.");
if (length < 14 + dataItems.Length)
throw new Exception("Not enough data received to parse individual item responses.");
IList<byte> itemResults = new ArraySegment<byte>(message, 14, dataItems.Length);
List<Exception> errors = null;
for (int i = 0; i < dataItems.Length; i++)
{
try
{
Plc.ValidateResponseCode((ReadWriteErrorCode)itemResults[i]);
}
catch(Exception e)
{
if (errors == null) errors = new List<Exception>();
errors.Add(new Exception($"Write of dataItem {dataItems[i]} failed: {e.Message}."));
}
}
if (errors != null)
throw new AggregateException(
$"Write failed for {errors.Count} items. See the innerExceptions for details.", errors);
}
private static class Header
{
public static byte[] Template { get; } =
{
0x03, 0x00, 0x00, 0x00, // TPKT
0x02, 0xf0, 0x80, // ISO DT
0x32, // S7 protocol ID
0x01, // JobRequest
0x00, 0x00, // Reserved
0x05, 0x00, // PDU reference
0x00, 0x0e, // Parameters length
0x00, 0x00, // Data length
0x05, // Function: Write var
0x00, // Number of items to write
};
public static class Offsets
{
public const int MessageLength = 2;
public const int ParameterSize = 13;
public const int DataLength = 15;
public const int ParameterCount = 18;
}
}
private static class Parameter
{
public static byte[] Template { get; } =
{
0x12, // Spec
0x0a, // Length of remaining bytes
0x10, // Addressing mode
0x02, // Transport size
0x00, 0x00, // Number of elements
0x00, 0x00, // DB number
0x84, // Area type
0x00, 0x00, 0x00 // Area offset
};
public static class Offsets
{
public const int WordLength = 3;
public const int Amount = 4;
public const int DbNumber = 6;
public const int Area = 8;
public const int Address = 9;
}
}
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using S7.Net.Types;
namespace S7.Net.Protocol
{
internal static class Serialization
{
public static ushort GetWordAt(IList<byte> buf, int index)
{
return (ushort)((buf[index] << 8) + buf[index]);
}
public static byte[] SerializeDataItem(DataItem dataItem)
{
if (dataItem.Value == null)
{
throw new Exception($"DataItem.Value is null, cannot serialize. StartAddr={dataItem.StartByteAdr} VarType={dataItem.VarType}");
}
if (dataItem.Value is string s)
{
switch (dataItem.VarType)
{
case VarType.S7String:
return S7String.ToByteArray(s, dataItem.Count);
case VarType.S7WString:
return S7WString.ToByteArray(s, dataItem.Count);
default:
return Types.String.ToByteArray(s, dataItem.Count);
}
}
return SerializeValue(dataItem.Value);
}
public static byte[] SerializeValue(object value)
{
switch (value.GetType().Name)
{
case "Boolean":
return new[] { (byte)((bool)value ? 1 : 0) };
case "Byte":
return Types.Byte.ToByteArray((byte)value);
case "Int16":
return Types.Int.ToByteArray((Int16)value);
case "UInt16":
return Types.Word.ToByteArray((UInt16)value);
case "Int32":
return Types.DInt.ToByteArray((Int32)value);
case "UInt32":
return Types.DWord.ToByteArray((UInt32)value);
case "Single":
return Types.Real.ToByteArray((float)value);
case "Double":
return Types.LReal.ToByteArray((double)value);
case "DateTime":
return Types.DateTime.ToByteArray((System.DateTime)value);
case "Byte[]":
return (byte[])value;
case "Int16[]":
return Types.Int.ToByteArray((Int16[])value);
case "UInt16[]":
return Types.Word.ToByteArray((UInt16[])value);
case "Int32[]":
return Types.DInt.ToByteArray((Int32[])value);
case "UInt32[]":
return Types.DWord.ToByteArray((UInt32[])value);
case "Single[]":
return Types.Real.ToByteArray((float[])value);
case "Double[]":
return Types.LReal.ToByteArray((double[])value);
case "String":
// Hack: This is backwards compatible with the old code, but functionally it's broken
// if the consumer does not pay attention to string length.
var stringVal = (string)value;
return Types.String.ToByteArray(stringVal, stringVal.Length);
case "DateTime[]":
return Types.DateTime.ToByteArray((System.DateTime[])value);
case "DateTimeLong[]":
return Types.DateTimeLong.ToByteArray((System.DateTime[])value);
default:
throw new InvalidVariableTypeException();
}
}
public static void SetAddressAt(ByteArray buffer, int index, int startByte, byte bitNumber)
{
var start = startByte * 8 + bitNumber;
buffer[index + 2] = (byte)start;
start >>= 8;
buffer[index + 1] = (byte)start;
start >>= 8;
buffer[index] = (byte)start;
}
public static void SetWordAt(ByteArray buffer, int index, ushort value)
{
buffer[index] = (byte)(value >> 8);
buffer[index + 1] = (byte)value;
}
}
}

View File

@@ -0,0 +1,31 @@
namespace S7.Net.Protocol
{
/// <summary>
/// Provides a representation of the Transport Service Access Point, or TSAP in short. TSAP's are used
/// to specify a client and server address. For most PLC types a default TSAP is available that allows
/// connection from any IP and can be calculated using the rack and slot numbers.
/// </summary>
public struct Tsap
{
/// <summary>
/// First byte of the TSAP.
/// </summary>
public byte FirstByte { get; set; }
/// <summary>
/// Second byte of the TSAP.
/// </summary>
public byte SecondByte { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Tsap" /> class using the specified values.
/// </summary>
/// <param name="firstByte">The first byte of the TSAP.</param>
/// <param name="secondByte">The second byte of the TSAP.</param>
public Tsap(byte firstByte, byte secondByte)
{
FirstByte = firstByte;
SecondByte = secondByte;
}
}
}

View File

@@ -0,0 +1,96 @@
using System;
namespace S7.Net.Protocol
{
/// <summary>
/// Implements a pair of TSAP addresses used to connect to a PLC.
/// </summary>
public class TsapPair
{
/// <summary>
/// The local <see cref="Tsap" />.
/// </summary>
public Tsap Local { get; set; }
/// <summary>
/// The remote <see cref="Tsap" />
/// </summary>
public Tsap Remote { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="TsapPair" /> class using the specified local and
/// remote TSAP.
/// </summary>
/// <param name="local">The local TSAP.</param>
/// <param name="remote">The remote TSAP.</param>
public TsapPair(Tsap local, Tsap remote)
{
Local = local;
Remote = remote;
}
/// <summary>
/// Builds a <see cref="TsapPair" /> that can be used to connect to a PLC using the default connection
/// addresses.
/// </summary>
/// <remarks>
/// The remote TSAP is constructed using <code>new Tsap(0x03, (byte) ((rack &lt;&lt; 5) | slot))</code>.
/// </remarks>
/// <param name="cpuType">The CPU type of the PLC.</param>
/// <param name="rack">The rack of the PLC's network card.</param>
/// <param name="slot">The slot of the PLC's network card.</param>
/// <returns>A TSAP pair that matches the given parameters.</returns>
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="cpuType"/> is invalid.
///
/// -or-
///
/// The <paramref name="rack"/> parameter is less than 0.
///
/// -or-
///
/// The <paramref name="rack"/> parameter is greater than 15.
///
/// -or-
///
/// The <paramref name="slot"/> parameter is less than 0.
///
/// -or-
///
/// The <paramref name="slot"/> parameter is greater than 15.</exception>
public static TsapPair GetDefaultTsapPair(CpuType cpuType, int rack, int slot)
{
if (rack < 0) throw InvalidRackOrSlot(rack, nameof(rack), "minimum", 0);
if (rack > 0x0F) throw InvalidRackOrSlot(rack, nameof(rack), "maximum", 0x0F);
if (slot < 0) throw InvalidRackOrSlot(slot, nameof(slot), "minimum", 0);
if (slot > 0x0F) throw InvalidRackOrSlot(slot, nameof(slot), "maximum", 0x0F);
switch (cpuType)
{
case CpuType.S7200:
return new TsapPair(new Tsap(0x10, 0x00), new Tsap(0x10, 0x01));
case CpuType.Logo0BA8:
// The actual values are probably on a per-project basis
return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x01, 0x02));
case CpuType.S7200Smart:
case CpuType.S71200:
case CpuType.S71500:
case CpuType.S7300:
case CpuType.S7400:
// Testing with S7 1500 shows only the remote TSAP needs to match. This might differ for other
// PLC types.
return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x03, (byte) ((rack << 5) | slot)));
default:
throw new ArgumentOutOfRangeException(nameof(cpuType), "Invalid CPU Type specified");
}
}
private static ArgumentOutOfRangeException InvalidRackOrSlot(int value, string name, string extrema,
int extremaValue)
{
return new ArgumentOutOfRangeException(name,
$"Invalid {name} value specified (decimal: {value}, hexadecimal: {value:X}), {extrema} value " +
$"is {extremaValue} (decimal) or {extremaValue:X} (hexadecimal).");
}
}
}

View File

@@ -0,0 +1,301 @@
using System;
using System.Text;
using S7.Net;
using S7.Net.Types;
namespace MES.Utility.Network.S7netplus
{
public class S7Helper : IDisposable
{
private object _lock = new object();
private Plc plc;
public S7Helper(CpuType cpuType, string ipAddress) : this(cpuType, ipAddress, 0, 1)
{
}
public S7Helper(CpuType cpuType, string ipAddress, short rack, short slot)
{
plc = new Plc(cpuType, ipAddress, 102, rack, slot);
plc.Open(1000);
}
#region BOOL类型读写
/// <summary>
/// 写入BOOL
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="value">true/false</param>
/// <returns></returns>
public bool WriteBool(string address, bool value)
{
lock (_lock)
try
{
plc.Write(address, value);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 读取bool类型
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="value">输出bool值</param>
/// <returns>返回false 读取失败PLC通信异常</returns>
public bool ReadBool(string address, out bool value)
{
lock (_lock)
try
{
value = (bool)plc.Read(address);
return true;
}
catch
{
value = false;
return false;
}
}
#endregion
#region INT16读写
public bool ReadInt16(string address, out ushort value)
{
lock (_lock)
try
{
value = (ushort)plc.Read(address);
return true;
}
catch
{
value = 0;
return false;
}
}
public bool WriteInt16(string address, ushort value)
{
lock (_lock)
try
{
plc.Write(address, value);
return true;
}
catch
{
return false;
}
}
#endregion
#region REAL读写
/// <summary>
///
/// </summary>
/// <param name="address">例如 DB1.DBD4.0</param>
/// <param name="value"></param>
/// <returns></returns>
public bool ReadSingle(string address, out float value)
{
lock (_lock)
try
{
value = ((uint)plc.Read(address)).ConvertToFloat();
return true;
}
catch
{
value = 0;
return false;
}
}
public bool ReadBytes(int db, int address, int count, out byte[] value)
{
try
{
value = plc.ReadBytes(DataType.DataBlock, db, address, count);
return true;
}
catch
{
value = null;
return false;
}
}
/// <summary>
///
/// </summary>
/// <param name="address">例如 DB1.DBD4.0</param>
/// <param name="value"></param>
/// <returns></returns>
public bool WriteSingle(string address, float value)
{
lock (_lock)
try
{
plc.Write(address, value);
return true;
}
catch
{
return false;
}
}
#endregion
#region String读写
/// <summary>
///
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="strLength">字符串长度</param>
/// <param name="value">返回字符串数据</param>
/// <returns></returns>
public bool ReadString(string address, int strLength, out string value)
{
lock (_lock)
try
{
string[] array = address.Split('.');
int db = Convert.ToInt32(array[0].Replace("DB", ""));
int startAddress = Convert.ToInt32(array[1].Replace("DBX", ""));
value = (string)plc.Read(DataType.DataBlock, db, startAddress, VarType.S7String, strLength);//获取对应长度的字符串
return true;
}
catch
{
value = "";
return false;
}
}
/// <summary>
/// 写入字符串
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="value">写入数据</param>
/// <param name="value">写入长度</param>
/// <returns></returns>
public bool WriteString(string address, string value, int strLength)
{
lock (_lock)
try
{
//转换address,取到DB 和 开始地址
string[] array = address.Split('.');
int db = Convert.ToInt32(array[0].Replace("DB", ""));
int startAddress = Convert.ToInt32(array[1].Replace("DBX", ""));
var bytes = S7.Net.Types.S7String.ToByteArray(value, strLength);
plc.WriteBytes(DataType.DataBlock, db, startAddress, bytes);
return true;
}
catch
{
return false;
}
}
#endregion
public bool ReadBytesString(string address, int strLength, out string value)
{
lock (_lock)
try
{
string[] array = address.Split('.');
int db = Convert.ToInt32(array[0].Replace("DB", ""));
int startAddress = Convert.ToInt32(array[1].Replace("DBX", ""));
byte[] values = plc.ReadBytes(DataType.DataBlock, db, startAddress, strLength);
value = Encoding.ASCII.GetString(values);
return true;
}
catch
{
value = "";
return false;
}
}
/// <summary>
/// 写入字符串
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="value">写入数据</param>
/// <param name="value">写入长度</param>
/// <returns></returns>
public bool WriteBytesString(string address, string value, int strLength)
{
lock (_lock)
try
{
//转换address,取到DB 和 开始地址
string[] array = address.Split('.');
int db = Convert.ToInt32(array[0].Replace("DB", ""));
int startAddress = Convert.ToInt32(array[1].Replace("DBX", ""));
var bytes = Encoding.ASCII.GetBytes(value);
if (bytes.Length > strLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({strLength}).");
var buffer = new byte[strLength];
Array.Copy(bytes, 0, buffer, 0, bytes.Length);
plc.WriteBytes(DataType.DataBlock, db, startAddress, buffer);
return true;
}
catch
{
return false;
}
}
#region INT32读写
/// <summary>
///
/// </summary>
/// <param name="address">例如DB1.DBD264.0</param>
/// <param name="value"></param>
/// <returns></returns>
public bool ReadInt32(string address, out int value)
{
lock (_lock)
try
{
value = (int)plc.Read(address);
return true;
}
catch
{
value = 0;
return false;
}
}
public bool WriteInt32(string address, int value)
{
lock (_lock)
try
{
plc.Write(address, value);
return true;
}
catch
{
return false;
}
}
#endregion
public void Dispose()
{
plc?.Close();
plc = null;
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// Extensions for Streams
/// </summary>
public static class StreamExtensions
{
/// <summary>
/// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
/// </summary>
/// <param name="stream">the Stream to read from</param>
/// <param name="buffer">the buffer to read into</param>
/// <param name="offset">the offset in the buffer to read into</param>
/// <param name="count">the amount of bytes to read into the buffer</param>
/// <returns>returns the amount of read bytes</returns>
public static int ReadExact(this Stream stream, byte[] buffer, int offset, int count)
{
int read = 0;
int received;
do
{
received = stream.Read(buffer, offset + read, count - read);
read += received;
}
while (read < count && received > 0);
return read;
}
/// <summary>
/// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
/// </summary>
/// <param name="stream">the Stream to read from</param>
/// <param name="buffer">the buffer to read into</param>
/// <param name="offset">the offset in the buffer to read into</param>
/// <param name="count">the amount of bytes to read into the buffer</param>
/// <returns>returns the amount of read bytes</returns>
public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
int read = 0;
int received;
do
{
received = await stream.ReadAsync(buffer, offset + read, count - read, cancellationToken).ConfigureAwait(false);
read += received;
}
while (read < count && received > 0);
return read;
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// Describes a TPKT Packet
/// </summary>
internal class TPKT
{
public byte Version;
public byte Reserved1;
public int Length;
public byte[] Data;
private TPKT(byte version, byte reserved1, int length, byte[] data)
{
Version = version;
Reserved1 = reserved1;
Length = length;
Data = data;
}
/// <summary>
/// Reads a TPKT from the socket Async
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <returns>Task TPKT Instace</returns>
public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var buf = new byte[4];
int len = await stream.ReadExactAsync(buf, 0, 4, cancellationToken).ConfigureAwait(false);
if (len < 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
var version = buf[0];
var reserved1 = buf[1];
var length = buf[2] * 256 + buf[3]; //BigEndian
var data = new byte[length - 4];
len = await stream.ReadExactAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false);
if (len < data.Length)
throw new TPKTInvalidException("TPKT payload incomplete / invalid");
return new TPKT
(
version: version,
reserved1: reserved1,
length: length,
data: data
);
}
public override string ToString()
{
return string.Format("Version: {0} Length: {1} Data: {2}",
Version,
Length,
BitConverter.ToString(Data)
);
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Bit from S7 plc to C#.
/// </summary>
public static class Bit
{
/// <summary>
/// Converts a Bit to bool
/// </summary>
public static bool FromByte(byte v, byte bitAdr)
{
return (((int)v & (1 << bitAdr)) != 0);
}
/// <summary>
/// Converts an array of bytes to a BitArray.
/// </summary>
/// <param name="bytes">The bytes to convert.</param>
/// <returns>A BitArray with the same number of bits and equal values as <paramref name="bytes"/>.</returns>
public static BitArray ToBitArray(byte[] bytes) => ToBitArray(bytes, bytes.Length * 8);
/// <summary>
/// Converts an array of bytes to a BitArray.
/// </summary>
/// <param name="bytes">The bytes to convert.</param>
/// <param name="length">The number of bits to return.</param>
/// <returns>A BitArray with <paramref name="length"/> bits.</returns>
public static BitArray ToBitArray(byte[] bytes, int length)
{
if (length > bytes.Length * 8) throw new ArgumentException($"Not enough data in bytes to return {length} bits.", nameof(bytes));
var bitArr = new BitArray(bytes);
var bools = new bool[length];
for (var i = 0; i < length; i++) bools[i] = bitArr[i];
return new BitArray(bools);
}
}
}

View File

@@ -0,0 +1,64 @@
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to read, set and reset bits inside bytes
/// </summary>
public static class Boolean
{
/// <summary>
/// Returns the value of a bit in a bit, given the address of the bit
/// </summary>
public static bool GetValue(byte value, int bit)
{
return (((int)value & (1 << bit)) != 0);
}
/// <summary>
/// Sets the value of a bit to 1 (true), given the address of the bit. Returns
/// a copy of the value with the bit set.
/// </summary>
/// <param name="value">The input value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to set.</param>
/// <returns>The modified value with the bit at index set.</returns>
public static byte SetBit(byte value, int bit)
{
SetBit(ref value, bit);
return value;
}
/// <summary>
/// Sets the value of a bit to 1 (true), given the address of the bit.
/// </summary>
/// <param name="value">The value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to set.</param>
public static void SetBit(ref byte value, int bit)
{
value = (byte) ((value | (1 << bit)) & 0xFF);
}
/// <summary>
/// Resets the value of a bit to 0 (false), given the address of the bit. Returns
/// a copy of the value with the bit cleared.
/// </summary>
/// <param name="value">The input value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to clear.</param>
/// <returns>The modified value with the bit at index cleared.</returns>
public static byte ClearBit(byte value, int bit)
{
ClearBit(ref value, bit);
return value;
}
/// <summary>
/// Resets the value of a bit to 0 (false), given the address of the bit
/// </summary>
/// <param name="value">The input value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to clear.</param>
public static void ClearBit(ref byte value, int bit)
{
value = (byte) (value & ~(1 << bit) & 0xFF);
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from bytes to byte arrays
/// </summary>
public static class Byte
{
/// <summary>
/// Converts a byte to byte array
/// </summary>
public static byte[] ToByteArray(byte value)
{
return new byte[] { value }; ;
}
/// <summary>
/// Converts a byte array to byte
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static byte FromByteArray(byte[] bytes)
{
if (bytes.Length != 1)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 1 bytes.");
}
return bytes[0];
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
namespace S7.Net.Types
{
class ByteArray
{
List<byte> list = new List<byte>();
public byte this[int index]
{
get => list[index];
set => list[index] = value;
}
public byte[] Array
{
get { return list.ToArray(); }
}
public int Length => list.Count;
public ByteArray()
{
list = new List<byte>();
}
public ByteArray(int size)
{
list = new List<byte>(size);
}
public void Clear()
{
list = new List<byte>();
}
public void Add(byte item)
{
list.Add(item);
}
public void AddWord(ushort value)
{
list.Add((byte) (value >> 8));
list.Add((byte) value);
}
public void Add(byte[] items)
{
list.AddRange(items);
}
public void Add(IEnumerable<byte> items)
{
list.AddRange(items);
}
public void Add(ByteArray byteArray)
{
list.AddRange(byteArray.Array);
}
}
}

View File

@@ -0,0 +1,354 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert a C# class to S7 data types
/// </summary>
public static class Class
{
private static IEnumerable<PropertyInfo> GetAccessableProperties(Type classType)
{
return classType
.GetProperties(
BindingFlags.SetProperty |
BindingFlags.Public |
BindingFlags.Instance)
.Where(p => p.GetSetMethod() != null);
}
private static double GetIncreasedNumberOfBytes(double numBytes, Type type, PropertyInfo propertyInfo)
{
switch (type.Name)
{
case "Boolean":
numBytes += 0.125;
break;
case "Byte":
numBytes = Math.Ceiling(numBytes);
numBytes++;
break;
case "Int16":
case "UInt16":
IncrementToEven(ref numBytes);
numBytes += 2;
break;
case "Int32":
case "UInt32":
IncrementToEven(ref numBytes);
numBytes += 4;
break;
case "Single":
IncrementToEven(ref numBytes);
numBytes += 4;
break;
case "Double":
IncrementToEven(ref numBytes);
numBytes += 8;
break;
case "String":
S7StringAttribute attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
IncrementToEven(ref numBytes);
numBytes += attribute.ReservedLengthInBytes;
break;
default:
var propertyClass = Activator.CreateInstance(type);
numBytes = GetClassSize(propertyClass, numBytes, true);
break;
}
return numBytes;
}
/// <summary>
/// Gets the size of the class in bytes.
/// </summary>
/// <param name="instance">An instance of the class</param>
/// <returns>the number of bytes</returns>
public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false)
{
var properties = GetAccessableProperties(instance.GetType());
foreach (var property in properties)
{
if (property.PropertyType.IsArray)
{
Type elementType = property.PropertyType.GetElementType();
Array array = (Array)property.GetValue(instance, null);
if (array.Length <= 0)
{
throw new Exception("Cannot determine size of class, because an array is defined which has no fixed size greater than zero.");
}
IncrementToEven(ref numBytes);
for (int i = 0; i < array.Length; i++)
{
numBytes = GetIncreasedNumberOfBytes(numBytes, elementType, property);
}
}
else
{
numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType, property);
}
}
if (false == isInnerProperty)
{
// enlarge numBytes to next even number because S7-Structs in a DB always will be resized to an even byte count
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
}
return numBytes;
}
private static object GetPropertyValue(Type propertyType, PropertyInfo propertyInfo, byte[] bytes, ref double numBytes)
{
object value = null;
switch (propertyType.Name)
{
case "Boolean":
// get the value
int bytePos = (int)Math.Floor(numBytes);
int bitPos = (int)((numBytes - (double)bytePos) / 0.125);
if ((bytes[bytePos] & (int)Math.Pow(2, bitPos)) != 0)
value = true;
else
value = false;
numBytes += 0.125;
break;
case "Byte":
numBytes = Math.Ceiling(numBytes);
value = (byte)(bytes[(int)numBytes]);
numBytes++;
break;
case "Int16":
IncrementToEven(ref numBytes);
// hier auswerten
ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
value = source.ConvertToShort();
numBytes += 2;
break;
case "UInt16":
IncrementToEven(ref numBytes);
// hier auswerten
value = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
numBytes += 2;
break;
case "Int32":
IncrementToEven(ref numBytes);
var wordBuffer = new byte[4];
Array.Copy(bytes, (int)numBytes, wordBuffer, 0, wordBuffer.Length);
uint sourceUInt = DWord.FromByteArray(wordBuffer);
value = sourceUInt.ConvertToInt();
numBytes += 4;
break;
case "UInt32":
IncrementToEven(ref numBytes);
var wordBuffer2 = new byte[4];
Array.Copy(bytes, (int)numBytes, wordBuffer2, 0, wordBuffer2.Length);
value = DWord.FromByteArray(wordBuffer2);
numBytes += 4;
break;
case "Single":
IncrementToEven(ref numBytes);
// hier auswerten
value = Real.FromByteArray(
new byte[] {
bytes[(int)numBytes],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 3] });
numBytes += 4;
break;
case "Double":
IncrementToEven(ref numBytes);
var buffer = new byte[8];
Array.Copy(bytes, (int)numBytes, buffer, 0, 8);
// hier auswerten
value = LReal.FromByteArray(buffer);
numBytes += 8;
break;
case "String":
S7StringAttribute attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
IncrementToEven(ref numBytes);
// get the value
var sData = new byte[attribute.ReservedLengthInBytes];
Array.Copy(bytes, (int)numBytes, sData, 0, sData.Length);
switch (attribute.Type)
{
case S7StringType.S7String:
value = S7String.FromByteArray(sData); break;
case S7StringType.S7WString:
value = S7WString.FromByteArray(sData); break;
default:
throw new ArgumentException("Please use a valid string type for the S7StringAttribute");
}
numBytes += sData.Length;
break;
default:
var propClass = Activator.CreateInstance(propertyType);
numBytes = FromBytes(propClass, bytes, numBytes);
value = propClass;
break;
}
return value;
}
/// <summary>
/// Sets the object's values with the given array of bytes
/// </summary>
/// <param name="sourceClass">The object to fill in the given array of bytes</param>
/// <param name="bytes">The array of bytes</param>
public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false)
{
if (bytes == null)
return numBytes;
var properties = GetAccessableProperties(sourceClass.GetType());
foreach (var property in properties)
{
if (property.PropertyType.IsArray)
{
Array array = (Array)property.GetValue(sourceClass, null);
IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType();
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{
array.SetValue(
GetPropertyValue(elementType, property, bytes, ref numBytes),
i);
}
}
else
{
property.SetValue(
sourceClass,
GetPropertyValue(property.PropertyType, property, bytes, ref numBytes),
null);
}
}
return numBytes;
}
private static double SetBytesFromProperty(object propertyValue, PropertyInfo propertyInfo, byte[] bytes, double numBytes)
{
int bytePos = 0;
int bitPos = 0;
byte[] bytes2 = null;
switch (propertyValue.GetType().Name)
{
case "Boolean":
// get the value
bytePos = (int)Math.Floor(numBytes);
bitPos = (int)((numBytes - (double)bytePos) / 0.125);
if ((bool)propertyValue)
bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true
else
bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false
numBytes += 0.125;
break;
case "Byte":
numBytes = (int)Math.Ceiling(numBytes);
bytePos = (int)numBytes;
bytes[bytePos] = (byte)propertyValue;
numBytes++;
break;
case "Int16":
bytes2 = Int.ToByteArray((Int16)propertyValue);
break;
case "UInt16":
bytes2 = Word.ToByteArray((UInt16)propertyValue);
break;
case "Int32":
bytes2 = DInt.ToByteArray((Int32)propertyValue);
break;
case "UInt32":
bytes2 = DWord.ToByteArray((UInt32)propertyValue);
break;
case "Single":
bytes2 = Real.ToByteArray((float)propertyValue);
break;
case "Double":
bytes2 = LReal.ToByteArray((double)propertyValue);
break;
case "String":
S7StringAttribute attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
switch (attribute.Type)
{
case S7StringType.S7String:
bytes2 = S7String.ToByteArray((string)propertyValue, attribute.ReservedLength); break;
case S7StringType.S7WString:
bytes2 = S7WString.ToByteArray((string)propertyValue, attribute.ReservedLength); break;
default:
throw new ArgumentException("Please use a valid string type for the S7StringAttribute");
}
break;
default:
numBytes = ToBytes(propertyValue, bytes, numBytes);
break;
}
if (bytes2 != null)
{
IncrementToEven(ref numBytes);
bytePos = (int)numBytes;
for (int bCnt = 0; bCnt < bytes2.Length; bCnt++)
bytes[bytePos + bCnt] = bytes2[bCnt];
numBytes += bytes2.Length;
}
return numBytes;
}
/// <summary>
/// Creates a byte array depending on the struct type.
/// </summary>
/// <param name="sourceClass">The struct object</param>
/// <returns>A byte array or null if fails.</returns>
public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0)
{
var properties = GetAccessableProperties(sourceClass.GetType());
foreach (var property in properties)
{
if (property.PropertyType.IsArray)
{
Array array = (Array)property.GetValue(sourceClass, null);
IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType();
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{
numBytes = SetBytesFromProperty(array.GetValue(i), property, bytes, numBytes);
}
}
else
{
numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), property, bytes, numBytes);
}
}
return numBytes;
}
private static void IncrementToEven(ref double numBytes)
{
numBytes = Math.Ceiling(numBytes);
if (numBytes % 2 > 0) numBytes++;
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Counter from S7 plc to C# ushort (UInt16).
/// </summary>
public static class Counter
{
/// <summary>
/// Converts a Counter (2 bytes) to ushort (UInt16)
/// </summary>
public static UInt16 FromByteArray(byte[] bytes)
{
if (bytes.Length != 2)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes.");
}
// bytes[0] -> HighByte
// bytes[1] -> LowByte
return (UInt16)((bytes[0] << 8) | bytes[1]);
}
/// <summary>
/// Converts a ushort (UInt16) to word (2 bytes)
/// </summary>
public static byte[] ToByteArray(UInt16 value)
{
byte[] bytes = new byte[2];
bytes[0] = (byte)((value << 8) & 0xFF);
bytes[1] = (byte)((value) & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of ushort (UInt16) to an array of bytes
/// </summary>
public static byte[] ToByteArray(UInt16[] value)
{
ByteArray arr = new ByteArray();
foreach (UInt16 val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of bytes to an array of ushort
/// </summary>
public static UInt16[] ToArray(byte[] bytes)
{
UInt16[] values = new UInt16[bytes.Length / 2];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 2; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,65 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert DInt from S7 plc to C# int (Int32).
/// </summary>
public static class DInt
{
/// <summary>
/// Converts a S7 DInt (4 bytes) to int (Int32)
/// </summary>
public static Int32 FromByteArray(byte[] bytes)
{
if (bytes.Length != 4)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
}
return bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3];
}
/// <summary>
/// Converts a int (Int32) to S7 DInt (4 bytes)
/// </summary>
public static byte[] ToByteArray(Int32 value)
{
byte[] bytes = new byte[4];
bytes[0] = (byte)((value >> 24) & 0xFF);
bytes[1] = (byte)((value >> 16) & 0xFF);
bytes[2] = (byte)((value >> 8) & 0xFF);
bytes[3] = (byte)((value) & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of int (Int32) to an array of bytes
/// </summary>
public static byte[] ToByteArray(Int32[] value)
{
ByteArray arr = new ByteArray();
foreach (Int32 val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of S7 DInt to an array of int (Int32)
/// </summary>
public static Int32[] ToArray(byte[] bytes)
{
Int32[] values = new Int32[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert DWord from S7 plc to C#.
/// </summary>
public static class DWord
{
/// <summary>
/// Converts a S7 DWord (4 bytes) to uint (UInt32)
/// </summary>
public static UInt32 FromByteArray(byte[] bytes)
{
return (UInt32)(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]);
}
/// <summary>
/// Converts 4 bytes to DWord (UInt32)
/// </summary>
public static UInt32 FromBytes(byte b1, byte b2, byte b3, byte b4)
{
return (UInt32)((b4 << 24) | (b3 << 16) | (b2 << 8) | b1);
}
/// <summary>
/// Converts a uint (UInt32) to S7 DWord (4 bytes)
/// </summary>
public static byte[] ToByteArray(UInt32 value)
{
byte[] bytes = new byte[4];
bytes[0] = (byte)((value >> 24) & 0xFF);
bytes[1] = (byte)((value >> 16) & 0xFF);
bytes[2] = (byte)((value >> 8) & 0xFF);
bytes[3] = (byte)((value) & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of uint (UInt32) to an array of S7 DWord (4 bytes)
/// </summary>
public static byte[] ToByteArray(UInt32[] value)
{
ByteArray arr = new ByteArray();
foreach (UInt32 val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of S7 DWord to an array of uint (UInt32)
/// </summary>
public static UInt32[] ToArray(byte[] bytes)
{
UInt32[] values = new UInt32[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,104 @@
using S7.Net.Protocol.S7;
using System;
namespace S7.Net.Types
{
/// <summary>
/// Create an instance of a memory block that can be read by using ReadMultipleVars
/// </summary>
public class DataItem
{
/// <summary>
/// Memory area to read
/// </summary>
public DataType DataType { get; set; }
/// <summary>
/// Type of data to be read (default is bytes)
/// </summary>
public VarType VarType { get; set; }
/// <summary>
/// Address of memory area to read (example: for DB1 this value is 1, for T45 this value is 45)
/// </summary>
public int DB { get; set; }
/// <summary>
/// Address of the first byte to read
/// </summary>
public int StartByteAdr { get; set; }
/// <summary>
/// Addess of bit to read from StartByteAdr
/// </summary>
public byte BitAdr { get; set; }
/// <summary>
/// Number of variables to read
/// </summary>
public int Count { get; set; }
/// <summary>
/// Contains the value of the memory area after the read has been executed
/// </summary>
public object Value { get; set; }
/// <summary>
/// Create an instance of DataItem
/// </summary>
public DataItem()
{
VarType = VarType.Byte;
Count = 1;
}
/// <summary>
/// Create an instance of <see cref="DataItem"/> from the supplied address.
/// </summary>
/// <param name="address">The address to create the DataItem for.</param>
/// <returns>A new <see cref="DataItem"/> instance with properties parsed from <paramref name="address"/>.</returns>
/// <remarks>The <see cref="Count" /> property is not parsed from the address.</remarks>
public static DataItem FromAddress(string address)
{
PLCAddress.Parse(address, out var dataType, out var dbNumber, out var varType, out var startByte,
out var bitNumber);
return new DataItem
{
DataType = dataType,
DB = dbNumber,
VarType = varType,
StartByteAdr = startByte,
BitAdr = (byte)(bitNumber == -1 ? 0 : bitNumber)
};
}
/// <summary>
/// Create an instance of <see cref="DataItem"/> from the supplied address and value.
/// </summary>
/// <param name="address">The address to create the DataItem for.</param>
/// <param name="value">The value to be applied to the DataItem.</param>
/// <returns>A new <see cref="DataItem"/> instance with properties parsed from <paramref name="address"/> and the supplied value set.</returns>
public static DataItem FromAddressAndValue<T>(string address, T value)
{
var dataItem = FromAddress(address);
dataItem.Value = value;
if (typeof(T).IsArray)
{
var array = ((Array)dataItem.Value);
if (array != null)
{
dataItem.Count = array.Length;
}
}
return dataItem;
}
internal static DataItemAddress GetDataItemAddress(DataItem dataItem)
{
return new DataItemAddress(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, Plc.VarTypeToByteLength(dataItem.VarType, dataItem.Count));
}
}
}

View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert between <see cref="T:System.DateTime"/> and S7 representation of datetime values.
/// </summary>
public static class DateTime
{
/// <summary>
/// The minimum <see cref="T:System.DateTime"/> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1990, 1, 1);
/// <summary>
/// The maximum <see cref="T:System.DateTime"/> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2089, 12, 31, 23, 59, 59, 999);
/// <summary>
/// Parses a <see cref="T:System.DateTime"/> value from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>A <see cref="T:System.DateTime"/> object representing the value read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of
/// <paramref name="bytes"/> is not 8 or any value in <paramref name="bytes"/>
/// is outside the valid range of values.</exception>
public static System.DateTime FromByteArray(byte[] bytes)
{
return FromByteArrayImpl(bytes);
}
/// <summary>
/// Parses an array of <see cref="T:System.DateTime"/> values from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>An array of <see cref="T:System.DateTime"/> objects representing the values read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of
/// <paramref name="bytes"/> is not a multiple of 8 or any value in
/// <paramref name="bytes"/> is outside the valid range of values.</exception>
public static System.DateTime[] ToArray(byte[] bytes)
{
if (bytes.Length % 8 != 0)
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing an array of DateTime requires a multiple of 8 bytes of input data, input data is '{bytes.Length}' long.");
var cnt = bytes.Length / 8;
var result = new System.DateTime[bytes.Length / 8];
for (var i = 0; i < cnt; i++)
result[i] = FromByteArrayImpl(new ArraySegment<byte>(bytes, i * 8, 8));
return result;
}
private static System.DateTime FromByteArrayImpl(IList<byte> bytes)
{
if (bytes.Count != 8)
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Count,
$"Parsing a DateTime requires exactly 8 bytes of input data, input data is {bytes.Count} bytes long.");
int DecodeBcd(byte input) => 10 * (input >> 4) + (input & 0b00001111);
int ByteToYear(byte bcdYear)
{
var input = DecodeBcd(bcdYear);
if (input < 90) return input + 2000;
if (input < 100) return input + 1900;
throw new ArgumentOutOfRangeException(nameof(bcdYear), bcdYear,
$"Value '{input}' is higher than the maximum '99' of S7 date and time representation.");
}
int AssertRangeInclusive(int input, byte min, byte max, string field)
{
if (input < min)
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is lower than the minimum '{min}' allowed for {field}.");
if (input > max)
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is higher than the maximum '{max}' allowed for {field}.");
return input;
}
var year = ByteToYear(bytes[0]);
var month = AssertRangeInclusive(DecodeBcd(bytes[1]), 1, 12, "month");
var day = AssertRangeInclusive(DecodeBcd(bytes[2]), 1, 31, "day of month");
var hour = AssertRangeInclusive(DecodeBcd(bytes[3]), 0, 23, "hour");
var minute = AssertRangeInclusive(DecodeBcd(bytes[4]), 0, 59, "minute");
var second = AssertRangeInclusive(DecodeBcd(bytes[5]), 0, 59, "second");
var hsec = AssertRangeInclusive(DecodeBcd(bytes[6]), 0, 99, "first two millisecond digits");
var msec = AssertRangeInclusive(bytes[7] >> 4, 0, 9, "third millisecond digit");
var dayOfWeek = AssertRangeInclusive(bytes[7] & 0b00001111, 1, 7, "day of week");
return new System.DateTime(year, month, day, hour, minute, second, hsec * 10 + msec);
}
/// <summary>
/// Converts a <see cref="T:System.DateTime"/> value to a byte array.
/// </summary>
/// <param name="dateTime">The DateTime value to convert.</param>
/// <returns>A byte array containing the S7 date time representation of <paramref name="dateTime"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value of
/// <paramref name="dateTime"/> is before <see cref="P:SpecMinimumDateTime"/>
/// or after <see cref="P:SpecMaximumDateTime"/>.</exception>
public static byte[] ToByteArray(System.DateTime dateTime)
{
byte EncodeBcd(int value)
{
return (byte) ((value / 10 << 4) | value % 10);
}
if (dateTime < SpecMinimumDateTime)
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 date time representation.");
if (dateTime > SpecMaximumDateTime)
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 date time representation.");
byte MapYear(int year) => (byte) (year < 2000 ? year - 1900 : year - 2000);
int DayOfWeekToInt(DayOfWeek dayOfWeek) => (int) dayOfWeek + 1;
return new[]
{
EncodeBcd(MapYear(dateTime.Year)),
EncodeBcd(dateTime.Month),
EncodeBcd(dateTime.Day),
EncodeBcd(dateTime.Hour),
EncodeBcd(dateTime.Minute),
EncodeBcd(dateTime.Second),
EncodeBcd(dateTime.Millisecond / 10),
(byte) (dateTime.Millisecond % 10 << 4 | DayOfWeekToInt(dateTime.DayOfWeek))
};
}
/// <summary>
/// Converts an array of <see cref="T:System.DateTime"/> values to a byte array.
/// </summary>
/// <param name="dateTimes">The DateTime values to convert.</param>
/// <returns>A byte array containing the S7 date time representations of <paramref name="dateTime"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any value of
/// <paramref name="dateTimes"/> is before <see cref="P:SpecMinimumDateTime"/>
/// or after <see cref="P:SpecMaximumDateTime"/>.</exception>
public static byte[] ToByteArray(System.DateTime[] dateTimes)
{
var bytes = new List<byte>(dateTimes.Length * 8);
foreach (var dateTime in dateTimes) bytes.AddRange(ToByteArray(dateTime));
return bytes.ToArray();
}
}
}

View File

@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert between <see cref="T:System.DateTime" /> and S7 representation of DateTimeLong (DTL) values.
/// </summary>
public static class DateTimeLong
{
public const int TypeLengthInBytes = 12;
/// <summary>
/// The minimum <see cref="T:System.DateTime" /> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1970, 1, 1);
/// <summary>
/// The maximum <see cref="T:System.DateTime" /> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2262, 4, 11, 23, 47, 16, 854);
/// <summary>
/// Parses a <see cref="T:System.DateTime" /> value from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>A <see cref="T:System.DateTime" /> object representing the value read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the length of
/// <paramref name="bytes" /> is not 12 or any value in <paramref name="bytes" />
/// is outside the valid range of values.
/// </exception>
public static System.DateTime FromByteArray(byte[] bytes)
{
return FromByteArrayImpl(bytes);
}
/// <summary>
/// Parses an array of <see cref="T:System.DateTime" /> values from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>An array of <see cref="T:System.DateTime" /> objects representing the values read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the length of
/// <paramref name="bytes" /> is not a multiple of 12 or any value in
/// <paramref name="bytes" /> is outside the valid range of values.
/// </exception>
public static System.DateTime[] ToArray(byte[] bytes)
{
if (bytes.Length % TypeLengthInBytes != 0)
{
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing an array of DateTimeLong requires a multiple of 12 bytes of input data, input data is '{bytes.Length}' long.");
}
var cnt = bytes.Length / TypeLengthInBytes;
var result = new System.DateTime[cnt];
for (var i = 0; i < cnt; i++)
{
var slice = new byte[TypeLengthInBytes];
Array.Copy(bytes, i * TypeLengthInBytes, slice, 0, TypeLengthInBytes);
result[i] = FromByteArrayImpl(slice);
}
return result;
}
private static System.DateTime FromByteArrayImpl(byte[] bytes)
{
if (bytes.Length != TypeLengthInBytes)
{
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing a DateTimeLong requires exactly 12 bytes of input data, input data is {bytes.Length} bytes long.");
}
var year = AssertRangeInclusive(Word.FromBytes(bytes[1], bytes[0]), 1970, 2262, "year");
var month = AssertRangeInclusive(bytes[2], 1, 12, "month");
var day = AssertRangeInclusive(bytes[3], 1, 31, "day of month");
var dayOfWeek = AssertRangeInclusive(bytes[4], 1, 7, "day of week");
var hour = AssertRangeInclusive(bytes[5], 0, 23, "hour");
var minute = AssertRangeInclusive(bytes[6], 0, 59, "minute");
var second = AssertRangeInclusive(bytes[7], 0, 59, "second");
;
var nanoseconds = AssertRangeInclusive<uint>(DWord.FromBytes(bytes[11], bytes[10], bytes[9], bytes[8]), 0,
999999999, "nanoseconds");
var time = new System.DateTime(year, month, day, hour, minute, second);
return time.AddTicks(nanoseconds / 100);
}
/// <summary>
/// Converts a <see cref="T:System.DateTime" /> value to a byte array.
/// </summary>
/// <param name="dateTime">The DateTime value to convert.</param>
/// <returns>A byte array containing the S7 DateTimeLong representation of <paramref name="dateTime" />.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the value of
/// <paramref name="dateTime" /> is before <see cref="P:SpecMinimumDateTime" />
/// or after <see cref="P:SpecMaximumDateTime" />.
/// </exception>
public static byte[] ToByteArray(System.DateTime dateTime)
{
if (dateTime < SpecMinimumDateTime)
{
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 DateTimeLong representation.");
}
if (dateTime > SpecMaximumDateTime)
{
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 DateTimeLong representation.");
}
var stream = new MemoryStream(TypeLengthInBytes);
// Convert Year
stream.Write(Word.ToByteArray(Convert.ToUInt16(dateTime.Year)), 0, 2);
// Convert Month
stream.WriteByte(Convert.ToByte(dateTime.Month));
// Convert Day
stream.WriteByte(Convert.ToByte(dateTime.Day));
// Convert WeekDay. NET DateTime starts with Sunday = 0, while S7DT has Sunday = 1.
stream.WriteByte(Convert.ToByte(dateTime.DayOfWeek + 1));
// Convert Hour
stream.WriteByte(Convert.ToByte(dateTime.Hour));
// Convert Minutes
stream.WriteByte(Convert.ToByte(dateTime.Minute));
// Convert Seconds
stream.WriteByte(Convert.ToByte(dateTime.Second));
// Convert Nanoseconds. Net DateTime has a representation of 1 Tick = 100ns.
// Thus First take the ticks Mod 1 Second (1s = 10'000'000 ticks), and then Convert to nanoseconds.
stream.Write(DWord.ToByteArray(Convert.ToUInt32(dateTime.Ticks % 10000000 * 100)), 0, 4);
return stream.ToArray();
}
/// <summary>
/// Converts an array of <see cref="T:System.DateTime" /> values to a byte array.
/// </summary>
/// <param name="dateTimes">The DateTime values to convert.</param>
/// <returns>A byte array containing the S7 DateTimeLong representations of <paramref name="dateTimes" />.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when any value of
/// <paramref name="dateTimes" /> is before <see cref="P:SpecMinimumDateTime" />
/// or after <see cref="P:SpecMaximumDateTime" />.
/// </exception>
public static byte[] ToByteArray(System.DateTime[] dateTimes)
{
var bytes = new List<byte>(dateTimes.Length * TypeLengthInBytes);
foreach (var dateTime in dateTimes)
{
bytes.AddRange(ToByteArray(dateTime));
}
return bytes.ToArray();
}
private static T AssertRangeInclusive<T>(T input, T min, T max, string field) where T : IComparable<T>
{
if (input.CompareTo(min) < 0)
{
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is lower than the minimum '{min}' allowed for {field}.");
}
if (input.CompareTo(max) > 0)
{
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is higher than the maximum '{max}' allowed for {field}.");
}
return input;
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# double.
/// </summary>
[Obsolete("Class Double is obsolete. Use Real instead for 32bit floating point, or LReal for 64bit floating point.")]
public static class Double
{
/// <summary>
/// Converts a S7 Real (4 bytes) to double
/// </summary>
public static double FromByteArray(byte[] bytes) => Real.FromByteArray(bytes);
/// <summary>
/// Converts a S7 DInt to double
/// </summary>
public static double FromDWord(Int32 value)
{
byte[] b = DInt.ToByteArray(value);
double d = FromByteArray(b);
return d;
}
/// <summary>
/// Converts a S7 DWord to double
/// </summary>
public static double FromDWord(UInt32 value)
{
byte[] b = DWord.ToByteArray(value);
double d = FromByteArray(b);
return d;
}
/// <summary>
/// Converts a double to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(double value) => Real.ToByteArray((float)value);
/// <summary>
/// Converts an array of double to an array of bytes
/// </summary>
public static byte[] ToByteArray(double[] value)
{
ByteArray arr = new ByteArray();
foreach (double val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of S7 Real to an array of double
/// </summary>
public static double[] ToArray(byte[] bytes)
{
double[] values = new double[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Int from S7 plc to C#.
/// </summary>
public static class Int
{
/// <summary>
/// Converts a S7 Int (2 bytes) to short (Int16)
/// </summary>
public static short FromByteArray(byte[] bytes)
{
if (bytes.Length != 2)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes.");
}
// bytes[0] -> HighByte
// bytes[1] -> LowByte
return (short)((int)(bytes[1]) | ((int)(bytes[0]) << 8));
}
/// <summary>
/// Converts a short (Int16) to a S7 Int byte array (2 bytes)
/// </summary>
public static byte[] ToByteArray(Int16 value)
{
byte[] bytes = new byte[2];
bytes[0] = (byte) (value >> 8 & 0xFF);
bytes[1] = (byte)(value & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of short (Int16) to a S7 Int byte array (2 bytes)
/// </summary>
public static byte[] ToByteArray(Int16[] value)
{
byte[] bytes = new byte[value.Length * 2];
int bytesPos = 0;
for(int i=0; i< value.Length; i++)
{
bytes[bytesPos++] = (byte)((value[i] >> 8) & 0xFF);
bytes[bytesPos++] = (byte) (value[i] & 0xFF);
}
return bytes;
}
/// <summary>
/// Converts an array of S7 Int to an array of short (Int16)
/// </summary>
public static Int16[] ToArray(byte[] bytes)
{
int shortsCount = bytes.Length / 2;
Int16[] values = new Int16[shortsCount];
int counter = 0;
for (int cnt = 0; cnt < shortsCount; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++] });
return values;
}
/// <summary>
/// Converts a C# int value to a C# short value, to be used as word.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static Int16 CWord(int value)
{
if (value > 32767)
{
value -= 32768;
value = 32768 - value;
value *= -1;
}
return (short)value;
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.IO;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# double.
/// </summary>
public static class LReal
{
/// <summary>
/// Converts a S7 LReal (8 bytes) to double
/// </summary>
public static double FromByteArray(byte[] bytes)
{
if (bytes.Length != 8)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 8 bytes.");
}
var buffer = bytes;
// sps uses bigending so we have to reverse if platform needs
if (BitConverter.IsLittleEndian)
{
Array.Reverse(buffer);
}
return BitConverter.ToDouble(buffer, 0);
}
/// <summary>
/// Converts a double to S7 LReal (8 bytes)
/// </summary>
public static byte[] ToByteArray(double value)
{
var bytes = BitConverter.GetBytes(value);
// sps uses bigending so we have to check if platform is same
if (BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
return bytes;
}
/// <summary>
/// Converts an array of double to an array of bytes
/// </summary>
public static byte[] ToByteArray(double[] value) => TypeHelper.ToByteArray(value, ToByteArray);
/// <summary>
/// Converts an array of S7 LReal to an array of double
/// </summary>
public static double[] ToArray(byte[] bytes) => TypeHelper.ToArray(bytes, FromByteArray);
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.IO;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# double.
/// </summary>
public static class Real
{
/// <summary>
/// Converts a S7 Real (4 bytes) to float
/// </summary>
public static float FromByteArray(byte[] bytes)
{
if (bytes.Length != 4)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
}
// sps uses bigending so we have to reverse if platform needs
if (BitConverter.IsLittleEndian)
{
// create deep copy of the array and reverse
bytes = new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
return BitConverter.ToSingle(bytes, 0);
}
/// <summary>
/// Converts a float to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(float value)
{
byte[] bytes = BitConverter.GetBytes(value);
// sps uses bigending so we have to check if platform is same
if (!BitConverter.IsLittleEndian) return bytes;
// create deep copy of the array and reverse
return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
/// <summary>
/// Converts an array of float to an array of bytes
/// </summary>
public static byte[] ToByteArray(float[] value)
{
var buffer = new byte[4 * value.Length];
var stream = new MemoryStream(buffer);
foreach (var val in value)
{
stream.Write(ToByteArray(val), 0, 4);
}
return buffer;
}
/// <summary>
/// Converts an array of S7 Real to an array of float
/// </summary>
public static float[] ToArray(byte[] bytes)
{
var values = new float[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Text;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from S7 strings to C# strings
/// An S7 String has a preceeding 2 byte header containing its capacity and length
/// </summary>
public static class S7String
{
private static Encoding stringEncoding = Encoding.ASCII;
/// <summary>
/// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default)
/// </summary>
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
public static Encoding StringEncoding
{
get => stringEncoding;
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
}
/// <summary>
/// Converts S7 bytes to a string
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string FromByteArray(byte[] bytes)
{
if (bytes.Length < 2)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / too short");
}
int size = bytes[0];
int length = bytes[1];
if (length > size)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / length larger than capacity");
}
try
{
return StringEncoding.GetString(bytes, 2, length);
}
catch (Exception e)
{
throw new PlcException(ErrorCode.ReadData,
$"Failed to parse {VarType.S7String} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
e);
}
}
/// <summary>
/// Converts a <see cref="T:string"/> to S7 string with 2-byte header.
/// </summary>
/// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns>
public static byte[] ToByteArray(string value, int reservedLength)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (reservedLength > 254) throw new ArgumentException($"The maximum string length supported is 254.");
var bytes = StringEncoding.GetBytes(value);
if (bytes.Length > reservedLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({reservedLength}).");
var buffer = new byte[2 + reservedLength];
Array.Copy(bytes, 0, buffer, 2, bytes.Length);
buffer[0] = (byte)reservedLength;
buffer[1] = (byte)bytes.Length;
return buffer;
}
}
}

View File

@@ -0,0 +1,67 @@
using System;
namespace S7.Net.Types
{
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public sealed class S7StringAttribute : Attribute
{
private readonly S7StringType type;
private readonly int reservedLength;
/// <summary>
/// Initializes a new instance of the <see cref="S7StringAttribute"/> class.
/// </summary>
/// <param name="type">The string type.</param>
/// <param name="reservedLength">Reserved length of the string in characters.</param>
/// <exception cref="ArgumentException">Please use a valid value for the string type</exception>
public S7StringAttribute(S7StringType type, int reservedLength)
{
if (!Enum.IsDefined(typeof(S7StringType), type))
throw new ArgumentException("Please use a valid value for the string type");
this.type = type;
this.reservedLength = reservedLength;
}
/// <summary>
/// Gets the type of the string.
/// </summary>
/// <value>
/// The string type.
/// </value>
public S7StringType Type => type;
/// <summary>
/// Gets the reserved length of the string in characters.
/// </summary>
/// <value>
/// The reserved length of the string in characters.
/// </value>
public int ReservedLength => reservedLength;
/// <summary>
/// Gets the reserved length in bytes.
/// </summary>
/// <value>
/// The reserved length in bytes.
/// </value>
public int ReservedLengthInBytes => type == S7StringType.S7String ? reservedLength + 2 : (reservedLength * 2) + 4;
}
/// <summary>
/// String type.
/// </summary>
public enum S7StringType
{
/// <summary>
/// ASCII string.
/// </summary>
S7String = VarType.S7String,
/// <summary>
/// Unicode string.
/// </summary>
S7WString = VarType.S7WString
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Text;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from S7 wstrings to C# strings
/// An S7 WString has a preceding 4 byte header containing its capacity and length
/// </summary>
public static class S7WString
{
/// <summary>
/// Converts S7 bytes to a string
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string FromByteArray(byte[] bytes)
{
if (bytes.Length < 4)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 WString / too short");
}
int size = (bytes[0] << 8) | bytes[1];
int length = (bytes[2] << 8) | bytes[3];
if (length > size)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 WString / length larger than capacity");
}
try
{
return Encoding.BigEndianUnicode.GetString(bytes, 4, length * 2);
}
catch (Exception e)
{
throw new PlcException(ErrorCode.ReadData,
$"Failed to parse {VarType.S7WString} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
e);
}
}
/// <summary>
/// Converts a <see cref="T:string"/> to S7 wstring with 4-byte header.
/// </summary>
/// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 4.</returns>
public static byte[] ToByteArray(string value, int reservedLength)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (reservedLength > 16382) throw new ArgumentException("The maximum string length supported is 16382.");
var buffer = new byte[4 + reservedLength * 2];
buffer[0] = (byte)((reservedLength >> 8) & 0xFF);
buffer[1] = (byte)(reservedLength & 0xFF);
buffer[2] = (byte)((value.Length >> 8) & 0xFF);
buffer[3] = (byte)(value.Length & 0xFF);
var stringLength = Encoding.BigEndianUnicode.GetBytes(value, 0, value.Length, buffer, 4) / 2;
if (stringLength > reservedLength) throw new ArgumentException($"The provided string length ({stringLength} is larger than the specified reserved length ({reservedLength}).");
return buffer;
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# float.
/// </summary>
[Obsolete("Class Single is obsolete. Use Real instead.")]
public static class Single
{
/// <summary>
/// Converts a S7 Real (4 bytes) to float
/// </summary>
public static float FromByteArray(byte[] bytes) => Real.FromByteArray(bytes);
/// <summary>
/// Converts a S7 DInt to float
/// </summary>
public static float FromDWord(Int32 value)
{
byte[] b = DInt.ToByteArray(value);
float d = FromByteArray(b);
return d;
}
/// <summary>
/// Converts a S7 DWord to float
/// </summary>
public static float FromDWord(UInt32 value)
{
byte[] b = DWord.ToByteArray(value);
float d = FromByteArray(b);
return d;
}
/// <summary>
/// Converts a double to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(float value) => Real.ToByteArray(value);
/// <summary>
/// Converts an array of float to an array of bytes
/// </summary>
public static byte[] ToByteArray(float[] value)
{
ByteArray arr = new ByteArray();
foreach (float val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of S7 Real to an array of float
/// </summary>
public static float[] ToArray(byte[] bytes)
{
float[] values = new float[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,37 @@
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from S7 Array of Chars (like a const char[N] C-String) to C# strings
/// </summary>
public class String
{
/// <summary>
/// Converts a string to <paramref name="reservedLength"/> of bytes, padded with 0-bytes if required.
/// </summary>
/// <param name="value">The string to write to the PLC.</param>
/// <param name="reservedLength">The amount of bytes reserved for the <paramref name="value"/> in the PLC.</param>
public static byte[] ToByteArray(string value, int reservedLength)
{
var length = value?.Length;
if (length > reservedLength) length = reservedLength;
var bytes = new byte[reservedLength];
if (length == null || length == 0) return bytes;
System.Text.Encoding.ASCII.GetBytes(value, 0, length.Value, bytes, 0);
return bytes;
}
/// <summary>
/// Converts S7 bytes to a string
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string FromByteArray(byte[] bytes)
{
return System.Text.Encoding.ASCII.GetString(bytes);
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace S7.Net.Types
{
/// <inheritdoc cref="S7String"/>
[Obsolete("Please use S7String class")]
public static class StringEx
{
/// <inheritdoc cref="S7String.FromByteArray(byte[])"/>
public static string FromByteArray(byte[] bytes) => S7String.FromByteArray(bytes);
/// <inheritdoc cref="S7String.ToByteArray(string, int)"/>
public static byte[] ToByteArray(string value, int reservedLength) => S7String.ToByteArray(value, reservedLength);
}
}

View File

@@ -0,0 +1,310 @@
using System;
using System.Linq;
using System.Reflection;
namespace S7.Net.Types
{
/// <summary>
/// Contains the method to convert a C# struct to S7 data types
/// </summary>
public static class Struct
{
/// <summary>
/// Gets the size of the struct in bytes.
/// </summary>
/// <param name="structType">the type of the struct</param>
/// <returns>the number of bytes</returns>
public static int GetStructSize(Type structType)
{
double numBytes = 0.0;
var infos = structType.GetFields();
foreach (var info in infos)
{
switch (info.FieldType.Name)
{
case "Boolean":
numBytes += 0.125;
break;
case "Byte":
numBytes = Math.Ceiling(numBytes);
numBytes++;
break;
case "Int16":
case "UInt16":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += 2;
break;
case "Int32":
case "UInt32":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += 4;
break;
case "Single":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += 4;
break;
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += 8;
break;
case "String":
S7StringAttribute attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += attribute.ReservedLengthInBytes;
break;
default:
numBytes += GetStructSize(info.FieldType);
break;
}
}
return (int)numBytes;
}
/// <summary>
/// Creates a struct of a specified type by an array of bytes.
/// </summary>
/// <param name="structType">The struct type</param>
/// <param name="bytes">The array of bytes</param>
/// <returns>The object depending on the struct type or null if fails(array-length != struct-length</returns>
public static object FromBytes(Type structType, byte[] bytes)
{
if (bytes == null)
return null;
if (bytes.Length != GetStructSize(structType))
return null;
// and decode it
int bytePos = 0;
int bitPos = 0;
double numBytes = 0.0;
object structValue = Activator.CreateInstance(structType);
var infos = structValue.GetType().GetFields();
foreach (var info in infos)
{
switch (info.FieldType.Name)
{
case "Boolean":
// get the value
bytePos = (int)Math.Floor(numBytes);
bitPos = (int)((numBytes - (double)bytePos) / 0.125);
if ((bytes[bytePos] & (int)Math.Pow(2, bitPos)) != 0)
info.SetValue(structValue, true);
else
info.SetValue(structValue, false);
numBytes += 0.125;
break;
case "Byte":
numBytes = Math.Ceiling(numBytes);
info.SetValue(structValue, (byte)(bytes[(int)numBytes]));
numBytes++;
break;
case "Int16":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// get the value
ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
info.SetValue(structValue, source.ConvertToShort());
numBytes += 2;
break;
case "UInt16":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// get the value
info.SetValue(structValue, Word.FromBytes(bytes[(int)numBytes + 1],
bytes[(int)numBytes]));
numBytes += 2;
break;
case "Int32":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// get the value
uint sourceUInt = DWord.FromBytes(bytes[(int)numBytes + 3],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 0]);
info.SetValue(structValue, sourceUInt.ConvertToInt());
numBytes += 4;
break;
case "UInt32":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// get the value
info.SetValue(structValue, DWord.FromBytes(bytes[(int)numBytes],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 3]));
numBytes += 4;
break;
case "Single":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// get the value
info.SetValue(structValue, Real.FromByteArray(new byte[] { bytes[(int)numBytes],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 3] }));
numBytes += 4;
break;
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// get the value
var data = new byte[8];
Array.Copy(bytes, (int)numBytes, data, 0, 8);
info.SetValue(structValue, LReal.FromByteArray(data));
numBytes += 8;
break;
case "String":
S7StringAttribute attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// get the value
var sData = new byte[attribute.ReservedLengthInBytes];
Array.Copy(bytes, (int)numBytes, sData, 0, sData.Length);
switch (attribute.Type)
{
case S7StringType.S7String:
info.SetValue(structValue, S7String.FromByteArray(sData));
break;
case S7StringType.S7WString:
info.SetValue(structValue, S7WString.FromByteArray(sData));
break;
default:
throw new ArgumentException("Please use a valid string type for the S7StringAttribute");
}
numBytes += sData.Length;
break;
default:
var buffer = new byte[GetStructSize(info.FieldType)];
if (buffer.Length == 0)
continue;
Buffer.BlockCopy(bytes, (int)Math.Ceiling(numBytes), buffer, 0, buffer.Length);
info.SetValue(structValue, FromBytes(info.FieldType, buffer));
numBytes += buffer.Length;
break;
}
}
return structValue;
}
/// <summary>
/// Creates a byte array depending on the struct type.
/// </summary>
/// <param name="structValue">The struct object</param>
/// <returns>A byte array or null if fails.</returns>
public static byte[] ToBytes(object structValue)
{
Type type = structValue.GetType();
int size = Struct.GetStructSize(type);
byte[] bytes = new byte[size];
byte[] bytes2 = null;
int bytePos = 0;
int bitPos = 0;
double numBytes = 0.0;
var infos = type.GetFields();
foreach (var info in infos)
{
bytes2 = null;
switch (info.FieldType.Name)
{
case "Boolean":
// get the value
bytePos = (int)Math.Floor(numBytes);
bitPos = (int)((numBytes - (double)bytePos) / 0.125);
if ((bool)info.GetValue(structValue))
bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true
else
bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false
numBytes += 0.125;
break;
case "Byte":
numBytes = (int)Math.Ceiling(numBytes);
bytePos = (int)numBytes;
bytes[bytePos] = (byte)info.GetValue(structValue);
numBytes++;
break;
case "Int16":
bytes2 = Int.ToByteArray((Int16)info.GetValue(structValue));
break;
case "UInt16":
bytes2 = Word.ToByteArray((UInt16)info.GetValue(structValue));
break;
case "Int32":
bytes2 = DInt.ToByteArray((Int32)info.GetValue(structValue));
break;
case "UInt32":
bytes2 = DWord.ToByteArray((UInt32)info.GetValue(structValue));
break;
case "Single":
bytes2 = Real.ToByteArray((float)info.GetValue(structValue));
break;
case "Double":
bytes2 = LReal.ToByteArray((double)info.GetValue(structValue));
break;
case "String":
S7StringAttribute attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
switch (attribute.Type)
{
case S7StringType.S7String:
bytes2 = S7String.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength); break;
case S7StringType.S7WString:
bytes2 = S7WString.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength); break;
default:
throw new ArgumentException("Please use a valid string type for the S7StringAttribute");
}
break;
}
if (bytes2 != null)
{
// add them
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
bytePos = (int)numBytes;
for (int bCnt = 0; bCnt < bytes2.Length; bCnt++)
bytes[bytePos + bCnt] = bytes2[bCnt];
numBytes += bytes2.Length;
}
}
return bytes;
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Converts the Timer data type to C# data type
/// </summary>
public static class Timer
{
/// <summary>
/// Converts the timer bytes to a double
/// </summary>
public static double FromByteArray(byte[] bytes)
{
double wert = 0;
wert = ((bytes[0]) & 0x0F) * 100.0;
wert += ((bytes[1] >> 4) & 0x0F) * 10.0;
wert += ((bytes[1]) & 0x0F) * 1.0;
// this value is not used... may for a nother exponation
//int unknown = (bytes[0] >> 6) & 0x03;
switch ((bytes[0] >> 4) & 0x03)
{
case 0:
wert *= 0.01;
break;
case 1:
wert *= 0.1;
break;
case 2:
wert *= 1.0;
break;
case 3:
wert *= 10.0;
break;
}
return wert;
}
/// <summary>
/// Converts a ushort (UInt16) to an array of bytes formatted as time
/// </summary>
public static byte[] ToByteArray(UInt16 value)
{
byte[] bytes = new byte[2];
bytes[1] = (byte)((int)value & 0xFF);
bytes[0] = (byte)((int)value >> 8 & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of ushorts (Uint16) to an array of bytes formatted as time
/// </summary>
public static byte[] ToByteArray(UInt16[] value)
{
ByteArray arr = new ByteArray();
foreach (UInt16 val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of bytes formatted as time to an array of doubles
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static double[] ToArray(byte[] bytes)
{
double[] values = new double[bytes.Length / 2];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 2; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace S7.Net.Types
{
internal static class TypeHelper
{
/// <summary>
/// Converts an array of T to an array of bytes
/// </summary>
public static byte[] ToByteArray<T>(T[] value, Func<T, byte[]> converter) where T : struct
{
var buffer = new byte[Marshal.SizeOf(default(T)) * value.Length];
var stream = new MemoryStream(buffer);
foreach (var val in value)
{
stream.Write(converter(val), 0, 4);
}
return buffer;
}
/// <summary>
/// Converts an array of T repesented as S7 binary data to an array of T
/// </summary>
public static T[] ToArray<T>(byte[] bytes, Func<byte[], T> converter) where T : struct
{
var typeSize = Marshal.SizeOf(default(T));
var entries = bytes.Length / typeSize;
var values = new T[entries];
for(int i = 0; i < entries; ++i)
{
var buffer = new byte[typeSize];
Array.Copy(bytes, i * typeSize, buffer, 0, typeSize);
values[i] = converter(buffer);
}
return values;
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Words from S7 plc to C#.
/// </summary>
public static class Word
{
/// <summary>
/// Converts a word (2 bytes) to ushort (UInt16)
/// </summary>
public static UInt16 FromByteArray(byte[] bytes)
{
if (bytes.Length != 2)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes.");
}
return (UInt16)((bytes[0] << 8) | bytes[1]);
}
/// <summary>
/// Converts 2 bytes to ushort (UInt16)
/// </summary>
public static UInt16 FromBytes(byte b1, byte b2)
{
return (UInt16)((b2 << 8) | b1);
}
/// <summary>
/// Converts a ushort (UInt16) to word (2 bytes)
/// </summary>
public static byte[] ToByteArray(UInt16 value)
{
byte[] bytes = new byte[2];
bytes[1] = (byte)(value & 0xFF);
bytes[0] = (byte)((value>>8) & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of ushort (UInt16) to an array of bytes
/// </summary>
public static byte[] ToByteArray(UInt16[] value)
{
ByteArray arr = new ByteArray();
foreach (UInt16 val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of bytes to an array of ushort
/// </summary>
public static UInt16[] ToArray(byte[] bytes)
{
UInt16[] values = new UInt16[bytes.Length/2];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length/2; cnt++)
values[cnt] = FromByteArray(new byte[] {bytes[counter++], bytes[counter++]});
return values;
}
}
}