1,博图数据块的数据排列原则:
数据对齐算法:
- 将当前地址对齐到整数:
numBytes = (int)Math.Ceiling(numBytes);
- 将当前地址对齐到偶整数:
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
2,数据对齐的原则,如果当前的数据类型是:
- bool,则使用当前地址.
- byte,则使用对齐方式1
- 其他,则使用对齐方式2,即偶数对齐的方法.
3,如何从地址和bool进行设定和取值,假设我们有一个byte[]数组代表整个DB,和一个浮点数代表bool的地址?
- 取值:
- 赋值
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;
bytePos = (int)Math.Floor(numBytes); bitPos = (int)((numBytes - (double)bytePos) / 0.125); if ((bool)obj) bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true else bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false
思路:获取当前bit所在的字节地址.然后使用 (注意位是从0开始的.位0..位15..位31.)
t=t | 1<<(bitPos)来进行置位单个位. (掩码置位,使用|,所有为1的位进行置位)
t=t &(~1<<(bitPos)来进行复位单个位,(掩码复位,使用 &(~掩码),则所有位1的掩码进行复位)
t=t^(1<<(bitPos) 来进行异或运算(,掩码的1所在的位进行取反操作.)
t=t&(1<<(bitPos))来判断某个位是否为1或为0.
2,迭代解析
numbytes: 迭代的plc地址
itemInfos:迭代的信息存储的列表
preName:迭代的名称.
ElementItem:用于解析的元素.
public static void DeserializeItem(ref double numBytes, List<ItemInfo> itemInfos, string preName, ElementItem item) { var PreName = (string.IsNullOrEmpty(preName)) ? "" : preName + "_"; var Info = new ItemInfo() { Name = PreName + item.Name, Type = item.Type }; switch (item.GetTypeInfo()) { case PlcParamType.BaseType: switch (item.Type) { case "Bool": Info.Addr = ParseAddr(numBytes, item); numBytes += 0.125; break; case "Char": case "Byte": numBytes = Math.Ceiling(numBytes); Info.Addr = ParseAddr(numBytes, item); numBytes++; ; break; case "Int": case "UInt": case "Word": numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; Info.Addr = ParseAddr(numBytes, item); numBytes += 2; ; break; case "DInt": case "UDInt": case "DWord": case "Time": case "Real": numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; Info.Addr = ParseAddr(numBytes, item); numBytes += 4; ; break; default: break; } itemInfos.Add(Info); break; case PlcParamType.String: numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; //---------- Info.Addr = ParseAddr(numBytes, item); numBytes += item.GetStringLength(); itemInfos.Add(Info); break; case PlcParamType.Array: //------------原程序的可能是个漏洞. numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; //------------- var elementType = item.GetElementType(); for (var i = 0; i < item.GetArrayLength(); i++) { var element = new ElementItem() { Name = item.Name + $"[{i}]", Type = elementType }; DeserializeItem(ref numBytes, itemInfos, PreName, element); } break; case PlcParamType.UDT: numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; //格式 foreach (var element in item.GetElementItems(Dict)) { DeserializeItem(ref numBytes, itemInfos, PreName + item.Name, element); } break; default: throw new ArgumentException("do not find type"); } }
思路: 如果元素的类型是基本类型:比如 bool,int,time...等等,则直接生成一条ItemInfo记录
如果元素的类型是数组,则获取数组的长度和获取数组的元素类型,然后进行迭代解析.
如果元素是UDT类型,也就是自定义的类型.那么就迭代获取元素,并且迭代解析.
----------------------
所有的这一切,源于一个Dictionary<string,List<ElementItem>>对象.这个对象存储了自定义的类别的名称和其子元素
比如有以下的一个DB导出文件;
则可以看到其有 "step"类型
元素有element1.....type为 int...
还有 "Recipe"类型
还有 "test1"类型
还有一个DataBlock,就是这个数据库,也将它当作一个类型,其type就是"数据块_1" ,其元素就是
名称 类型
Recipe1 Recipe(这个可以在上面找到)
关键是其内部的Struct结构,对于这个结构,我们自定义生成其类型 为 Name_Struct.
TYPE "step" VERSION : 0.1 STRUCT element1 : Int; element2 : Int; element3 : Int; element4 : Int; element5 : Int; element6 : Int; element7 : Real; element8 : Real; element9 : Real; element10 : Real; element11 : Real; element12 : Real; element13 : Real; element14 : Bool; element15 : Bool; element16 : Int; END_STRUCT; END_TYPE TYPE "Recipe" VERSION : 0.1 STRUCT "Name" : String[20]; steps : Array[0..29] of "step"; END_STRUCT; END_TYPE TYPE "test1" VERSION : 0.1 STRUCT b : Bool; END_STRUCT; END_TYPE DATA_BLOCK "数据块_1" { S7_Optimized_Access := 'FALSE' } VERSION : 0.1 NON_RETAIN STRUCT Recipe1 : "Recipe"; AAA : "Recipe"; aa : String; adef : Struct a : Bool; b : Bool; c : Int; bool1 : Bool; bool2 : Bool; ffff : Struct ttt : Bool; aaa : Bool; fff : Bool; eefg : Bool; END_STRUCT; afe : Bool; aaaaaaa : "test1"; x1 : "test1"; x2 : "test1"; x3 : Array[0..1] of Byte; abcef : Array[0..10] of Struct aef : Struct Static_1 : Bool; Static_2 : Bool; END_STRUCT; eef : Bool; affe : Bool; END_STRUCT; END_STRUCT; END_STRUCT; BEGIN Recipe1.steps[29].element14 := FALSE; END_DATA_BLOCK
3,从db文件中读取类信息.
//从db文件中读取类信息,并且放入到Dict之中,同时填充MainBlock信息,如果有的话. public static void GetTypeFromFile(Dictionary<string, List<ElementItem>> Dict, string path, out ElementItem MainBlock) { MainBlock = new ElementItem();//生成Block对象. using (Stream st = new FileStream(path, FileMode.Open))//读取文本文件DB块中的数据. { StreamReader reader = new StreamReader(st); List<ElementItem> list; while (!reader.EndOfStream) { string line = reader.ReadLine();//读取一行数据进行判断 switch (GetReaderLineTYPE(line))//通过解析函数解析这一行的数据是什么类型. { case "TYPE"://如果是类型 对应 db文件里面 TYPE xxxx 的格式 list = new List<ElementItem>();//则创建一个列表准备容纳该TYPE的ELementItem,也就是元素集. string tn = GetReaderLineName(line);//解析type行的名称.也就是该type的名称. GetElementsFromReader(reader, list, tn, Dict);//然后调用函数进行将元素放入列表,最后哪个Dictionary参数用于接受内联Struct类型. Dict[tn] = list;//将该类型在字典中生成名值对,也就是Type名称,List<elementItem>作为值. break; case "DATA_BLOCK": MainBlock = new ElementItem(); string bn = GetReaderLineName(line); MainBlock.Name = bn; MainBlock.Type = bn; list = new List<ElementItem>(); GetElementsFromReader(reader, list, bn, Dict);//如果是DB块,则填充Main Block(备用),剩下的根上面一样). Dict[bn] = list; break; default: break; } } } }
4, 辅助的读取迭代函数,实现的关键...
public static void GetElementsFromReader(StreamReader reader, List<ElementItem> list, string type_name, Dictionary<string, List<ElementItem>> Dict) { ElementItem item; Tuple<string, string> tp; while (!reader.EndOfStream) { string line = reader.ReadLine();//首先,其必须是一个解析Type或者DataBlock的过程,因为只有这两处调用它. switch (GetReaderLineTYPE(line))//解析每行的类别. { case "ELEMENT"://当解析到该行是元素的时候.就将该行加入到列表中. item = new ElementItem(); tp = GetElementInfo(line); item.Name = tp.Item1; item.Type = tp.Item2; list.Add(item); break; case "StructELEMENT"://当解析到该行是Struct时,也就是元素类型时Struct的时候, item = new ElementItem(); tp = GetElementInfo(line); item.Name = tp.Item1; item.Type = tp.Item2.Remove(tp.Item2.LastIndexOf("Struct"));//由于Array Struct的存在,将该元素类型从....Struct //替换为 .... elementName_Struct item.Type = item.Type + type_name + "_" + item.Name + "_" + "Struct";// string structType = type_name + "_" + item.Name + "_" + "Struct";//该名称为其新的类别. list.Add(item);//首先将该元素加入列表. List<ElementItem> sub = new List<ElementItem>();//将该子类别(我们自定义的类别,添加到字典中. GetElementsFromReader(reader, sub, structType, Dict); Dict[structType] = sub; break; case "END_STRUCT"://当接受到这个时,表明一个Type或者一个DataBlock的解析工作结束了,返回上层对象. return; default: break; } } }
5,工具函数1,用于帮助判断DB文件每行的信息.
private static Tuple<string, string> GetElementInfo(string line) { if (!GetReaderLineTYPE(line).Contains("ELEMENT")) throw new Exception("this line is not element " + line); int pos = line.IndexOf(" : "); string Name = line.Remove(pos).Trim(' ', ';'); var t = Name.IndexOf(' '); if (t > 0) Name = Name.Remove(t); string Type = line.Substring(pos + 3).Trim(' ', ';'); if (Type.Contains(" :=")) Type = Type.Remove(Type.IndexOf(" :=")); return new Tuple<string, string>(Name, Type); } private static string GetReaderLineName(string line) { if ((GetReaderLineTYPE(line) != "TYPE") && (GetReaderLineTYPE(line) != "DATA_BLOCK")) throw new Exception("not read name of " + line); return line.Substring(line.IndexOf(' ')).Trim(' '); } private static string GetReaderLineTYPE(string line) { //Console.WriteLine(line); if (line.Contains("TYPE ")) return "TYPE"; if (line.Contains("DATA_BLOCK ")) return "DATA_BLOCK"; if (line.Contains("END_STRUCT;")) return "END_STRUCT"; if (line.Trim(' ') == "STRUCT") return "STRUCT"; if (line.EndsWith("Struct")) return "StructELEMENT"; if ((line.EndsWith(";"))) return "ELEMENT"; return null; }
6,从之前生成的字典和MainDataBlock中生成 List<Item Infos>也就是展开DB块信息.
public static void DeserializeItem(ref double numBytes, List<ItemInfo> itemInfos, string preName, ElementItem item) { var PreName = (string.IsNullOrEmpty(preName)) ? "" : preName + "_";//如果前导名称不为空,则添加到展开的名称上去. //这里是个bug,最后将这行放到string生成,或者BaseType生成上,因为会出现多次_ var Info = new ItemInfo() { Name = PreName + item.Name, Type = item.Type }; switch (item.GetTypeInfo())//解析这个元素的类别 { case PlcParamType.BaseType://基类直接添加. switch (item.Type) { case "Bool": Info.Addr = ParseAddr(numBytes, item); numBytes += 0.125; break; case "Char": case "Byte": numBytes = Math.Ceiling(numBytes); Info.Addr = ParseAddr(numBytes, item); numBytes++; ; break; case "Int": case "UInt": case "Word": numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; Info.Addr = ParseAddr(numBytes, item); numBytes += 2; ; break; case "DInt": case "UDInt": case "DWord": case "Time": case "Real": numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; Info.Addr = ParseAddr(numBytes, item); numBytes += 4; ; break; default: break; } itemInfos.Add(Info); break; case PlcParamType.String://string类型.直接添加 numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; //---------- Info.Addr = ParseAddr(numBytes, item); numBytes += item.GetStringLength();//如果是string,则加256,否则加 xxx+2; itemInfos.Add(Info); break; case PlcParamType.Array://数组则进行分解,再迭代. //------------原程序的可能是个漏洞. numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; //------------- var elementType = item.GetElementType(); for (var i = 0; i < item.GetArrayLength(); i++) { var element = new ElementItem() { Name = item.Name + $"[{i}]", Type = elementType }; DeserializeItem(ref numBytes, itemInfos, PreName, element); } break; case PlcParamType.UDT://PLC类型,进行分解后迭代. numBytes = Math.Ceiling(numBytes); if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) numBytes++; //格式 foreach (var element in item.GetElementItems(Dict)) { DeserializeItem(ref numBytes, itemInfos, PreName + item.Name, element); } break; default: throw new ArgumentException("do not find type"); } }
注意,每条信息组成:(s
public class ItemInfo { public ItemInfo() { } public string Name { get; set; }//element的名称 public string Type { get; set; }//element的类别(基类别,比如字符串,byte,bool之类. public object Addr { get; internal set; }//地址;比如dbx0.0之类.
//综合就是给出 PLC变量名 PLC变量类型 PLC变量地址 的一个列表. }
7,功能扩展,支持多个db文件的解析操作
// MainFunction to read message from dbs. //迭代解析 多个db文件. public static void GetDBInfosFromFiles(string path, Dictionary<string, List<ElementItem>> Dict, Dictionary<string, List<ItemInfo>> DataBlocks) { DirectoryInfo dir = new DirectoryInfo(path); FileInfo[] files = dir.GetFiles("*.db"); List<ElementItem> blocks = new List<ElementItem>(); foreach (var file in files)//将每个文件的db块加入到 db数组中,再将解析的TYPE都加入到字典中. { ElementItem MainBlock; GetTypeFromFile(Dict, file.FullName, out MainBlock); if (MainBlock != null) { MainBlock.Name = file.Name.Remove(file.Name.IndexOf('.')); blocks.Add(MainBlock); } } foreach (var block in blocks)//然后迭代解析每个DB块的信息,将求加入到第二个字典中. { double numBytes = 0.0; List<ItemInfo> itemInfos = new List<ItemInfo>(); DeserializeItem(ref numBytes, itemInfos, "", block); DataBlocks[block.Name] = itemInfos; } }
8,使用CSVHELPER类从昆仑通态导出的文件中来读取信息
public static MacInfos[] GetCSVInfosFromFile(string path,string[] infos) { //用csvhelper类读取数据:注意,必须用这个方法读取,否则是乱码! using (var reader = new StreamReader(path, Encoding.GetEncoding("GB2312"))) { using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { //读取4行无效信息............. csv.Read(); infos[0] = csv.GetField(0); csv.Read(); infos[1] = csv.GetField(0); csv.Read(); infos[2] = csv.GetField(0); csv.Read(); infos[3] = csv.GetField(0); //读取所有的数据放入一个数组中.标准用法. var records = csv.GetRecords<MacInfos>().ToArray(); return records; } } }
9,将CSV文件的变量名进行填充,即将 Dict之中的变量名填充到 CSV的变量名之中.
//用于将填充完的信息数组反写回csv文件. public static void WriteIntoCSVOfMAC(string path) { string csvpath = FindFiles(path, "*.csv").First();//找到csv文件. string[] strinfos = new string[4];//填充无效信息的4行 MacInfos[] macInfos = GetCSVInfosFromFile(csvpath,strinfos);//获取MacInfos数组和无效信息字符串数组. foreach(var key in DBInfos.Keys)//轮询每个DB块. { var infos = (from info in macInfos where GetDBFromMacInfo(info) == key.Remove(key.IndexOf('_')) select info).ToArray();//将找到的Macinfo中再去查找对应的DB块的Macinfos[]数组. WriteDBToMacInfos(key, infos);//然后将对应的db块的信息,(找到其中的元素的名称,一一写入对应infos的变量名之中. } //填充完所有的Macinfos数组后,将这个数组反写回CSV文件. using (var writer = new StreamWriter(new FileStream(csvpath, FileMode.Open), Encoding.GetEncoding("GB2312"))) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { //将原来的无效信息反填充回去. csv.WriteField(strinfos[0]); csv.NextRecord();//转移到下一行.去读写数据. csv.WriteField(strinfos[1]); csv.NextRecord(); csv.WriteField(strinfos[2]); csv.NextRecord(); csv.WriteField(strinfos[3]); csv.NextRecord(); //然后填充数组. csv.WriteRecords(macInfos); } }
10,结论:
1,在使用的时候,首先导出DB块的块生成源,
2,然后将所有的*.db文件们放入c:macfile文件夹之中.
3,使用昆仑通态软件导入标签功能,导入db块和导入utc块,将变量导入到软件中,这个时候,变量名一栏是空的.
4,使用导出设备信息,将导出一个csv文件.
5,运行小软件.结束.
有喜欢工控软件开发的多多沟通交流.