与下位机或设备的通信解析优化的一点功能:T4+动态编译

时间:2023-03-09 04:37:05
与下位机或设备的通信解析优化的一点功能:T4+动态编译
    去年接触的一个项目中,需要通过TCP与设备进行对接的,传的是Modbus协议的数据,然后后台需要可以动态配置协议解析的方式,即寄存器的解析方式,,配置信息有:Key,数据Index,源数据类型,数据库列类型,数据排列方式 

一开始使用的方式是,从数据库读取出协议的配置,然后在接收到数据的时候,循环每个配置项根据配置-----解析数据------转换类型----存临时列表,,后来改进了一下,配置项存缓存,,数据库修改的时候,同时更新缓存。。

但还是慢了一点,因为需一个配置大概是20-30个数据项,每一条数据都要for循环20-30次 ,再加上还有N个根据配置的数据类型去做转换的if判断,这么一套下来,也很耗时间,但待解析的数据量大的情况下,,相对也很耗资源。。。

最后的觉得方案是:利用T4生成C#的class源码+运行时编译成类,数据直接扔class里直接解析出结果,不需要循环,也不需要if判断,因为在t4生成源码的时候,已经根据配置处理完了,因此节省了很多的时间。

不过由于T4模板的IDE支持的很不好,不过好在运行时T4模板在IDE内生成出来的类是partial的,因此,可以把大部分的代码,放在外部的C#文件里。先来看数据项的配置信息:

  public class DataItem
{
/// <summary>
/// 数据项ID
/// </summary>
public ObjectId DataItemID { set; get; } /// <summary>
/// 偏移量
/// </summary>
public int Pos { set; get; } /// <summary>
/// 大小
/// </summary>
public int Size { set; get; } public int BitIndex { set; get; } /// <summary>
/// 数据项数据库储存类型
/// </summary>
public DbDataTypeEnum DbType { set; get; } /// <summary>
/// 数据项协议源字节数组中的数据类型
/// </summary>
public DataTypeEnum SourceType { set; get; } /// <summary>
/// 计算因子
/// </summary>
public decimal Factor { set; get; } public string Key { set; get; }
} /// <summary>
/// 对应的数据库字段类型
/// </summary>
public enum DbDataTypeEnum
{
Int32 = , Int64 = , Double = , DateTime = , Decimal = , Boolean =
} public enum DataTypeEnum
{
Int = , Short = , Datetime = , Long = , Decimal = , UInt = , Byte = , Boolean = , Bit = , UShort = , UByte =
}

这里为什么要区分源数据和数据库数据类型呢?主要是因为设备一般是int,short,double,float等类型,但是,对应到数据库,有时候比如说使用mongodb,之类的数据库,不一定有完全匹配的,因此需要区分两种数据项,

再来就是T4的模板  ProtocolExecuteTemplate.tt:

 <#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="Kugar.Core.NetCore" #>
<#@ assembly name="Kugar.Device.Service.BLL" #>
<#@ assembly name="Kugar.Device.Service.Data" #>
<#@ assembly name="MongoDB.Bson" #> <#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="Kugar.Core.BaseStruct" #>
<#@ import namespace="MongoDB.Bson" #> using System;
using System.Text;
using Kugar.Core.BaseStruct;
using Kugar.Core.ExtMethod;
using Kugar.Core.Log;
using Kugar.Device.Service.Data.DTO;
using Kugar.Device.Service.Data.Enums;
using MongoDB.Bson; namespace Kugar.Device.Service.BLL
{
<#
var className="ProtocolExecutor_" + Protocol.Version.Replace('.','_') + "_" + this.GetNextClasID();
#> public class <#=className #>:IProtocolExecutor
{
private string _version="";
private ObjectId _protocolID;
private readonly DateTime _baseDt=TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(, , )); public <#=className #> (ObjectId protocolID, string version)
{
_version=version;
_protocolID=protocolID;
} public ObjectId ProtocolID {get{return _protocolID;}} public BsonDocument Execute(byte[] data, int startIndex)
{
BsonDocument bson=new BsonDocument();
<#
foreach(var item in Protocol.Items){ #>
bson["<#=item.Key #>"]= <#= DecodeConfig(item,) #>;
<#
}
#> return bson; }
}
}

在直接在循环里输出解析后的语句,并且生成的类名记得后面加多一个随机数。。。。

然后再加一个ProtocolExecuteTemplate.Part.cs的部分类,补全T4模板的功能,因为在T4里IDE支持的不好,,写代码确实难受,,没直接写C#舒服:

 public partial class ProtocolExecuteTemplate
{
private static int _classID = ; public ProtocolExecuteTemplate(DTO_ProtocolDataItem protocol)
{
Protocol = protocol; } public DTO_ProtocolDataItem Protocol { set; get; } public string DecodeConfig(DTO_ProtocolDataItem.DataItem item,int startIndex)
{
var str = ""; switch (item.SourceType)
{
case DataTypeEnum.Int:
str = $"BitConverter.ToInt32(data,startIndex + {startIndex + item.Pos})";
break;
case DataTypeEnum.UInt:
str = $"BitConverter.ToUInt32(data,startIndex + {startIndex + item.Pos})";
break;
case DataTypeEnum.Short:
str = $"BitConverter.ToInt16(data,startIndex + {startIndex + item.Pos})";
break;
case DataTypeEnum.Long:
str= $"BitConverter.ToInt64(data,startIndex + {startIndex + item.Pos})";
break;
case DataTypeEnum.Byte:
case DataTypeEnum.UByte:
case DataTypeEnum.Bit:
str = $"data[startIndex + {startIndex + item.Pos}]";
break;
case DataTypeEnum.UShort:
str = $"BitConverter.ToUInt16(data,startIndex+{startIndex + item.Pos})";
break;
default:
throw new ArgumentOutOfRangeException();
} if (item.SourceType==DataTypeEnum.Bit)
{
return byteToBit(str, item.BitIndex);
}
else
{
return valueTODBType(str, item.Factor, item.DbType);
}
} private string valueTODBType(string sourceValue, decimal factor, DbDataTypeEnum dbType)
{
switch (dbType)
{
case DbDataTypeEnum.Int32:
return $" new BsonInt32({(factor > 0 ? $"(int)({factor}{getDecimalShortChar(factor)} * {sourceValue})" : sourceValue)})";
case DbDataTypeEnum.Int64:
return $" new BsonInt64({(factor > 0 ? $"(long)({factor}{getDecimalShortChar(factor)} * {sourceValue})" : sourceValue)})";
case DbDataTypeEnum.Double:
return $"new BsonDouble({(factor > 0 ? $"(double)({factor}{getDecimalShortChar(factor)} * {sourceValue})" : $"(double){sourceValue}")})";
case DbDataTypeEnum.DateTime:
return $"new BsonDateTime(_baseDt.AddSeconds({sourceValue}))";
case DbDataTypeEnum.Decimal:
return $"new Decimal128({(factor > 0 ? $"(decimal)({factor}{getDecimalShortChar(factor)} * {sourceValue})" : sourceValue)})";
default:
throw new ArgumentOutOfRangeException(nameof(dbType), dbType, null);
}
} private string byteToBit(string data, int index)
{
switch (index)
{
case :
{ return $"(({data} & 1) ==1)";//(data & 1) == 1;
}
case :
{
return $"(({data} & 2) ==1)";// (data & 2) == 2;
}
case :
{
return $"(({data} & 4) ==1)";//(data & 4) == 4;
}
case :
{
return $"(({data} & 8) ==1)";//(data & 8) == 8;
}
case :
{
return $"(({data} & 16) ==1)";//(data & 16) == 16;
}
case :
{
return $"(({data} & 32) ==1)";//(data & 32) == 32;
}
case :
{
return $"(({data} & 64) ==1)";//(data & 64) == 64;
}
case :
{
return $"(({data} & 128) ==1)";//(data & 128) == 128;
}
default:
throw new ArgumentOutOfRangeException(nameof(index));
} return $"(({data} & {index + 1}) ==1)";
} /// <summary>
/// 用于判断传入的fator是否需要使用deciaml进行运算,如果有小数点的,则是否decimal缩写m,,如果没有小数点,则使用普通的int类型
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private string getDecimalShortChar(decimal value)
{
return (value % ) == ? "" : "m";
} public int GetNextClasID()
{
return Interlocked.Increment(ref _classID);
}
}

这样,在运行时,即可直接生成可用于解析的类了,而且也不需要for循环判断,生成出来的类如:

     public class ProtocolExecutor_1_1_000
{
public BsonDocument Execute(byte[] data, int startIndex)
{
BsonDocument bson = new BsonDocument(); bson["项目名1"] = new BsonInt32(BitConverter.ToInt32(data, startIndex + 偏移量));
bson["项目名2"] = new BsonInt32(BitConverter.ToInt32(data, startIndex + 偏移量));
。。。。。。。 //其他数据项 return bson;
}
}

到这一步,就可以根绝配置项生成出对应的C#代码了,剩下的就是动态编译的事情了、将该代码编译出运行时Type,然后传入数据----解析出数据