解析博图数据块(昆仑通态触摸屏自动命名)
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,运行小软件.结束.
附上git地址和,
有喜欢工控软件开发的多多沟通交流.