-
对西门子PLC中Struct的读写
S7NetPlus库中也提供了.ReadStruct();方法来对PLC中的Struct进行读写操作:
//首先需要在C#中定义一个和PLC中的Struct对应的结构体
//结构体中就连字段的声明顺序,都必须要和PLC中保持一致
public struct PlcStruct{
public bool varBool;
public byte varByte;
public ushort varWord;
public uint varDW;
public int varDInt;
[S7String(S7StringType.S7String,254)]
public string varS7String;
[S7String(S7StringType.S7WString,254)]
public string varS7WString;
public double varLreal;
}
//之后就可以进行读写操作了
PlcStruct myStruct = new PlcStruct();
//参数1是DBid,参数2是开始字节地址
myStruct = (PlcStruct)plcObj.ReadStruct<PlcStruct>(3,806);
但是在查看数据时,发现DWord数据发生了“错误”,PLC中明明是777_777_77(16#04A2_CB71),读取到的数据却是190_917_069_2(16#71CB_A204),转换成十六进制后发现数据的大小端颠倒了。当然可以通过把读取到的DWord数据转成字节数组再倒过来,来获得正确的数值,不过这不是重点;PLC中还有自定义的数据类型(UDT),使用.ReadStruct();是读不到的,而且.ReadClass();方法也读不到,在这里还是卡了很久,最终找到一个方法可以较方便的解决读Struct或UDT或“类”的问题:
plcObj.ReadBytes();只要知道从哪里开始读(startByteAdr)以及读多少个字节(Count),就可以把整个结构体或者自定义类型(UDT)获取到字节数组,之后再根据当中具体的地址进行数据类型转换,最后赋值到C#定义的struct/Class中即可:
//在我的PLC的DB3中,结构体的开始地址为806,最后一地址是1594(Lreal类型是64位占8个字节),所以一共要读取788个字节
int startByteAdr = 806;
var structByteArray = plcObj.ReadBytes(DataType.DataBlock,3,startByteAdr,788);
myStruct.varBool = Bit.FromByte(structByteArray[0], 0);
myStruct.varByte = structByteArray[807 - startBtyeAdr];
myStruct.varWord = Word.FromByteArray(structByteArray[(808 - startBtyeAdr)..(810 - startBtyeAdr)]);
myStruct.vadDW = DWord.FromByteArray(structByteArray[(810 - startBtyeAdr)..(814 - startBtyeAdr)]);
myStruct.varDInt = DInt.FromByteArray(structByteArray[(814 - startBtyeAdr)..(818 - startBtyeAdr)]);
myStruct.varS7String = S7String.FromByteArray(structByteArray[(818 - startBtyeAdr)..(1074 - startBtyeAdr)]);
myStruct.varS7WString = S7WString.FromByteArray(structByteArray[(1074 - startBtyeAdr)..(1586 - startBtyeAdr)]);
myStruct.varLreal = LReal.FromByteArray(structByteArray[(1586 - startBtyeAdr)..(1594 - startBtyeAdr)]);
读取PLC中自定义类型UDT的时候,操作没多大差别,可以先在C#定义一个类(class),之后跟结构体一样的操作进行字节的读取。
C#8中提供了方便的通过数组索引来获取到数组范围的方式,但是在framework里不行,可以通过linq中的skip()+take()方法,例:structByteArray.Skip("PLC中偏移减去开始偏移地址").Take("该数据类型所占字节数").ToArray();
往PLC中写入struct就很简单,只需要实例化一个结构体,然后将其中的字段赋值,并调用.WriteStruct();方法即可,但要注意的是,C#中定义的struct里的字段,数据类型顺序一定要与PLC中结构体的数据类型排列顺序一致,否则执行写入时会使PLC中的数据混乱。
写入Class的时候,遇到了一些困难,S7NetPlus库中提供的ReadClass();与WriteClass();方法不知道为什么既读不出来,也写不进PLC中,压根用不了(也可能是我博途还不怎么会用的原因);根据上述读写struct的逻辑,只好自己写一个WriteClass的拓展方法:
//拓展方法---静态类中的静态方法
public static class S7NetClassOperationExtension{
public static void WriteClassEx(this Plc plc,object classValue, DataType dataType,int DBid,int startByteAdr){
//利用反射判断类中字段的数据类型,依据类型对字段的值进行与PLC数据类型相对应的字节数组转换
//最后将这些数组拼接成一整个数组,通过S7NetPlus中WriteBytes();方法写入到博途中
//创建List<byte[]>用于存储PLC数据类型的字节数组
List<byte[]> listBytes = new List<byte[]>();
var classFields = classValue.GetType().GetFields();
foreach(var field in classFields)
{
//先获取该字节的值
var varObj = field.GetValue(classValue);
//每次遍历都声明一个新字节数组
byte[] varBytes = new byte[]{};
//判断字段的数据类型,再将字段的值转换成对应的字节数组
swicth(field.FieldType.Name)
{
case "Double":
varBytes = Lreal.ToByteArray((double)varObj);
break;
case "Single":
varBytes = Real.ToByteArray((float)varObj);
break;
case "UInt32":
varBytes = DWord.ToByteArray((uint)varObj);
break;
case "Uint16":
varBytes = Word.ToByteArray((ushort)varObj);
break;
case "Int32":
varBytes = DInt.ToByteArray((int)varObj);
break;
case "Int16":
varBytes = S7.Net.Types.Int.ToByteArray((short)varObj);
break;
case "Byte":
//在PLC中要占两个字节的位置
varBytes = new byte[]{(byte)varObj,0};
break;
case "Boolean":
//在PLC中要占两个字节的位置
varBytes = new byte[]{Convert.ToByte((bool)varObj)};
break;
//处理String类型时,还需要判断该字段的特性是S7String还是S7WString
case "String":
var att = field.GetCustomAttribute<S7StringAttribute>();
swicth(att.Type.ToString())
{
case "S7String":
varBytes = S7String.ToByteArray(varObj.ToString(),254);
break;
case "S7WString":
varBytes = S7WString.ToByteArray(varObj.ToString(),254);
break;
}
break;
}
listBytes.Add(varBytes);
//将list中的byte数组展平成一个字节数组
byte[] combineClassBytes = listBytes.SelectMany(arr => arr).ToArray();
//执行字节写入操作
plc.WriteBytes(dataType,DBid,startByteAdr,combineClassBytes);
}
}
}
-
上面的WriteClassEx();方法只是一个简单的,实际使用中,PLC的UDT自定义数据类型可能是复杂的(比如层层嵌套或包含数组),基于此,利用反射与递归,整了个相对完整版的方法,希望能够应对一些实际情况:
//拓展方法--静态类中的静态方法
public static class S7NetPlusExtension{
public static void WriteClassEnhance(this Plc plc,DataType datatype,int DBid,int StartByteAdr,Object classValue){
byte[] byteArrayFromClass = ToByteArrayForClass(classValue);
plc.WriteBytes(datatype,DBid,startByteAdr,classValue);
}
private static byte[] ToByteArrayForClass(Object classValue){
//创建一个List<Byte[]>存储各种PLC类型数据的字节数组
List<byte[]> listBytes = new List<byte[]>();
//获取类中的字段集合
var classFields = classValue.GetType().GetFields();
//遍历每一个字段
foreach(var field in classFields){
//获取当前字段的值
var fieldValue = field.GetValue(classValue);
//声明一个字节数组用来临时保存转换的字节数据
byte[] varBytes = new byte[]{};
//判断当前字段的值是否是基元类型或string类型,如果是就直接转换
if(fieldValue.IsNullOrPrimitiveOrString()){
varBytes = ByteArrayFromFieldInfo(field,fieldValue);
}
//判断是不是数组
else if(field is Array arr){
varBytes = ByteArrayFromArray(arr);
}
else
{
//既不是基元类型也不是数组,就只能还是class类了,递归
varBytes = ToByteArrayForClass(fieldValue);
}
listBytes.Add(varBytes);
//将List展开成字节数组
var resBytes = listBytes.SelectMany(arr => arr).ToArray();
return resBytes;
}
}
//拓展方法,判断该数据类型是否为空,是否是基元类型,是否是string类型
private static bool IsNullOrPrimitiveOrString(this object obj){
if(obj == null) return true;
Type ty = obj.GetType();
return ty.IsPrimitive || ty == typeof(string);
}
//判断字段的类型,并且将值转换为相应的PLC类型需要的字节数组
private static byte[] ByteArrayFromFieldInfo(FieldInfo field,object fieldValue){
//临时数组
byte[] varBytes = new byte[]{};
switch(field.FieldType.Name){
case "Double":
resBytes = LReal.ToByteArray((double)fieldValue);
break;
case "Single":
resBytes = Real.ToByteArray((float)fieldValue);
break;
case "UInt32":
resBytes = DWord.ToByteArray((uint)fieldValue);
break;
case "UInt16":
resBytes = Word.ToByteArray((ushort)fieldValue);
break;
case "Int32":
resBytes = DInt.ToByteArray((int)fieldValue);
break;
case "Int16":
resBytes = S7.Net.Types.Int.ToByteArray((short)fieldValue);
break;
case "Byte":
resBytes = new byte[] { (byte)fieldValue, 0 };
break;
case "Boolean":
resBytes = new byte[] { Convert.ToByte((bool)fieldValue), 0 };
break;
case "String":
var att = field.GetCustomAttribute<S7StringAttribute>();
switch (att.Type.ToString())
{
case "S7String":
resBytes = S7String.ToByteArray(fieldValue.ToString(), 254);
break;
case "S7WString":
resBytes = S7WString.ToByteArray(fieldValue.ToString(), 254);
break;
}
break;
}
return resBytes;
}
//判断数组的数据类型,并转换成PLC所对应的字节数组
private static byte[] ByteArrayFromArray(Array arr){
List<byte[]> listBytes = new List<byte[]>();
foreach(var element in arr){
byte[] varBytes = new byte[]{};
//判断数组类型是否为基元类型或string,如果是就直接转换
if(element.IsNullOrPrimitiveOrString()){
switch (element.GetType().Name)
{
//考虑到空间占用,应该很少出现S7String/S7WString类型的数组,所以偷懒了
case "Double":
varBytes = LReal.ToByteArray((double)element);
break;
case "Single":
varBytes = Real.ToByteArray((float)element);
break;
case "UInt32":
varBytes = DWord.ToByteArray((uint)element);
break;
case "UInt16":
varBytes = Word.ToByteArray((ushort)element);
break;
case "Int32":
varBytes = DInt.ToByteArray((int)element);
break;
case "Int16":
varBytes = S7.Net.Types.Int.ToByteArray((short)element);
break;
case "Byte":
varBytes = S7.Net.Types.Byte.ToByteArray((byte)element);
break;
case "Boolean":
varBytes = new byte[] { Convert.ToByte((bool)element)};
break;
}
}
else
{
//如果是别的类的话,就递归
varBytes = ToByteArrayForClass(element);
}
listBytes.Add(varBytes);
}
//在PLC中,byte与bit类型数据结尾还要加一个字节{0}
if (arr.GetType().Name == "Byte[]" || arr.GetType().Name == "Boolean[]") {
listBytes.Add(new byte[] { 0 });
}
var resBytes = listBytes.SelectMany(arr => arr).ToArray();
return resBytes;
}
}
- 本节完