# GWDriverDemo.STD **Repository Path**: salior2019/GWDriverDemo.STD ## Basic Information - **Project Name**: GWDriverDemo.STD - **Description**: 协议插件开发示例代码,介绍了如何开发一个协议插件,继承框架的CEquipBase类后,重写基类中的几个方法来实现当前协议的内在逻辑。其中Init负责传入设备所需的连接参数,GetData负责一个周期内获取获取设备数据,GetYC和GetYX负责对获取到的数据进行赋值。 - **Primary Language**: C# - **License**: LGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-05-15 - **Last Updated**: 2025-05-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # GWDriverDemo.STD ### 介绍 协议插件开发示例代码 ### 软件架构 ![img.png](./doc/img.png) 由软件架构示意图可知,系统会在调用init初始化成功之后调用GetData更新数据点位值。 SetParm可对点位值进行操作更新。 ### 安装教程 将生成的文件复制到软件目录的dll下 ![img.png](./doc/img-1.png) ### 使用说明 该demo使用随机值进行实际场景的更新。 ##### 初始化设备相关参数 ```csharp /// /// 设备通信间隔时间 /// 此次也跟Local_Addr相关, /// private int _sleepInterval = 0; /// /// 设备连接参数信息,如果连接参数很简单,可以不用定义对象。 /// private ConnectionConfig _connectionConfig; /// /// 当前设备的实时数据 /// private Dictionary _currentValue; /// /// 当前设备的实时事件,开发者需要处理相同事件内容重复产生的问题。 /// private List _currentEvents; /// /// 自定义当前设备的事件名称级别 /// 如果有多个事件名称需要定义,可以在自定义参数中进行定义 /// private int _defaultEventMessageLevel = 0; /// /// 初始化方法 /// 在界面添加完成后,会进入到该方法进行初始化 /// 之后再界面修改连接参数后,会再一次进入该方法。 /// /// equip表对象属性 /// public override bool init(EquipItem item) { /* item.Equip_addr 设备地址 解释:通常存放设备的唯一标识或者设备的连接地址。这里需要根据具体的协议来区分,如果一对一的直连设备 item.communication_param 设备连接参数 解释:通常存放设备的连接信息,具体由当前协议插件来约定,在配置文档中写明即可。 item.Local_addr 通讯端口(也叫通讯线程),任意字符,不宜过长。 解释:在Equip表,你可能会发现不少设备的Local_addr字段可能都是空的,也可能都是一个具体的字符串。 我们按照该字段的值进行Group By归类后,就得到了同一个值的设备数量有多少个,这个就代表一个线程管控了多少个设备。 item.communication_time_param 解释:在设备线程组里面,一个设备多久通信一次,即多久采集一次数据,单位毫秒。 如果communication_time_param职能比较多,也可以将多个参数的拼接,此时需要自行处理拆分后再转换。 配置举例:假设1个线程管控10个设备,要求每个设备每秒采集一次数据,那么这个字段的值应不大于100毫秒。其他场景同理计算即可。 item.Reserve2 设备自定义参数 解释:一般一些连接参数较多,需要规范化存储时,可以将属性放到自定义参数中,直观一些。当然也可以使用其他字段去拼接起来,但不建议这样做。 在6.1版本中,该字段在数据库中存储的值为一个JSON格式的数据。 在低版本中可以按照JSON格式来存储这个数据。 */ //获取设备连接通讯的间隔时间。 _ = int.TryParse(item.communication_time_param, out _sleepInterval); /* 在构造连接参数数,根据实际情况,以下展示一个连接参数模型的赋值。 如果连接参数简单,也可以使用自定义连接参数,直接使用communication_param更好,减少配置项,这里需要开发人员自己确定好。 */ if (!string.IsNullOrWhiteSpace(item.Reserve2)) { var dictParams = JsonConvert.DeserializeObject>(item.Reserve2); _connectionConfig = new ConnectionConfig { ServerUrl = item.Equip_addr, UserName = dictParams.TryGetValue("UserName", out var userName) ? userName : string.Empty, Password = dictParams.TryGetValue("Password", out var password) ? password : string.Empty, CertificatePath = dictParams.TryGetValue("CertificatePath", out var certPath) ? certPath : string.Empty, CertificatePwd = dictParams.TryGetValue("CertificatePwd", out var certPwd) ? certPwd : string.Empty, }; //我们可以定义多个事件名称的级别,命名方式如DefaultEventMessageLevel,如果未取到,默认值给0,但最好要区分好,因为使用0的事件级别很多场景都使用。 _ = int.TryParse(dictParams.TryGetValue("DefaultEventMessageLevel", out var defaultEventMessageLevelStr) ? defaultEventMessageLevelStr : "0", out _defaultEventMessageLevel); } return base.init(item); } ``` ##### 建立设备连接 ```csharp /// /// 在设备管理界面编辑后,会重新进入该方法,一般用于处理首次连接及重连 /// 对于设备的连接地址,连接账号密码发生更改后,可以进行重连。 /// /// public override bool OnLoaded() { //TODO 这里可以写于设备连接的具体代码了。根据_connectionConfig连接参数,去创建自己的连接对象。 ConnClientManager.Instance.CreateClientSession(_connectionConfig); //返回默认值 return base.OnLoaded(); } ``` ##### 获取设备状态及实时数据 ```csharp /// /// 获取当前设备连接的数据 /// 注意要控制好该方法不要出异常,否则会出现设备一直处于初始化状态中 /// /// 设备基类对象 /// public override CommunicationState GetData(CEquipBase pEquip) { //通过等待间隔时间,来达到多久取一次的。 base.Sleep(_sleepInterval); //当然开发者也可以在此次在增加相关业务逻辑。 //获取当前连接地址的状态 var equipStatus = ConnClientManager.Instance.GetClientSessionStatus(_connectionConfig.ServerUrl); //如果连接状态正常,设置为在线 if (equipStatus) { //只有在线是才采集数据 _currentValue = ConnClientManager.Instance.GetCurrentValues(_connectionConfig.ServerUrl, pEquip.m_equip_no); return CommunicationState.ok; } else { //否则设置离线 return CommunicationState.fail; } } ``` ##### 遥测点数据更新 ```csharp /// /// 获取遥测 /// /// ycp表对象属性(不是全部) /// public override bool GetYC(YcpTableRow r) { /* 注意:在此处最好不用打印日志,因为这里会产生大量的日志,如果需要调试某个点位时,可以在自定义参数里面加参数,针对固定的遥测进行日志调试。 r.main_instruction 操作命令,如EquipCurrentInfo r.minor_instruction 操作参数,如Temperature,Humidness等 r.Reserve2 自定义参数,以json结构存储,同设备的自定义参数一样。 在给遥测赋值时提供了诸多方法,支持单个类型,多元组类型,可以根据实际需要使用。 SetYCData(YcpTableRow r, object o); SetYCDataNoRead(IQueryable Rows); SetYcpTableRowData(YcpTableRow r, float o); SetYcpTableRowData(YcpTableRow r, (double, double, double, double, double, double) o); SetYcpTableRowData(YcpTableRow r, string o); SetYcpTableRowData(YcpTableRow r, int o); SetYcpTableRowData(YcpTableRow r, double o); SetYcpTableRowData(YcpTableRow r, (double, double, double, double, double, double, double) o); SetYcpTableRowData(YcpTableRow r, (double, double) o); SetYcpTableRowData(YcpTableRow r, (DateTime, double) o); SetYcpTableRowData(YcpTableRow r, (double, double, double, double) o); SetYcpTableRowData(YcpTableRow r, (double, double, double, double, double) o); SetYcpTableRowData(YcpTableRow r, (double, double, double) o); */ /* 实时数据示例代码,可以根据自己的业务进行处理*/ if (_currentValue == null) return true; try { //此处的Key值需要根据实际情况去处理。如果构造实时数据缓存字典是需要由开发去定义。 //总的来说,按照设备+遥测遥信的方式构造缓存数据是比较合理的。 string key = r.equip_no + "_" + r.main_instruction; if (_currentValue.ContainsKey(key)) { var objValue = _currentValue[key]; if (objValue == null) SetYCData(r, "");//此处不可以设置为null。 else SetYCData(r, objValue); } else { SetYCData(r, "***"); } } catch (Exception ex) { SetYCData(r, "测点赋值出现异常,请查看日志"); DataCenter.Write2Log($"记录报错日志:{ex}", LogLevel.Error); } //此处默认都返回true,否则设备会处于离线。 return true; } ``` ##### 遥信点数据更新 ```csharp /// /// 更新遥信点数据 /// /// yxp表对象属性(不是全部) /// public override bool GetYX(YxpTableRow r) { /* 注意:在此处最好不用打印日志,因为这里会产生大量的日志,如果需要调试某个点位时,可以在自定义参数里面加参数,针对固定的遥测进行日志调试。 r.main_instruction 操作命令,如EquipCurrentInfo r.minor_instruction 操作参数,如Temperature,Humidness等 r.Reserve2 自定义参数,以json结构存储,同设备的自定义参数一样。 在给遥测赋值时提供了诸多方法,支持bool、string类型,正常使用bool就够了,特殊情况可自行处理。 SetYXData(YxpTableRow r, object o); SetYxpTableRowData(YxpTableRow r, string o); SetYxpTableRowData(YxpTableRow r, bool o); */ /* 实时数据示例代码,可以根据自己的业务进行处理*/ if (_currentValue == null) return false; try { string key = r.equip_no + "_" + r.main_instruction; if (_currentValue.ContainsKey(key)) { var nodeIdObj = _currentValue[key]; if (nodeIdObj == null) SetYXData(r, "***"); else SetYXData(r, nodeIdObj); } else { SetYXData(r, "***"); } } catch (Exception ex) { SetYXData(r, "遥信赋值出现异常,请查看日志"); DataCenter.Write2Log($"记录报错日志:{ex}", LogLevel.Error); } return true; } ``` ##### 设备事件发布 ```csharp /// /// 事件 /// 如门禁设备的一些通行记录数据。 /// 如果对事件记录实时性有非常高的要求,可以接收到事件后直接转。 /// /// public override bool GetEvent() { //从当前设备连接中获取事件列表 _currentEvents = ConnClientManager.Instance.GetCurrentEvents(_connectionConfig.ServerUrl, this.m_equip_no); if (_currentEvents == null) return true; //假设_currentEvents对象每次都是新的数据,不存在旧数据,需开发者自行处理好. foreach (var eventItem in _currentEvents) { //EquipEvent中的事件级别根据当前事件名称定义好的级别。便于北向上报数据时的甄别。 var evt = new EquipEvent(JsonConvert.SerializeObject(eventItem),"可以自定义的消息格式", (MessageLevel)_defaultEventMessageLevel, DateTime.Now); EquipEventList.Add(evt); } _currentEvents = null; //循环完成后,将事件记录置空,避免下次重复产生相同的事件. return true; } ``` ##### 设备命令下发 ```csharp /// /// 设备命令下发 /// /// 操作命令 /// 操作参数 /// 传入的值 /// public override bool SetParm(string mainInstruct, string minorInstruct, string value) { /* 注意:建议在此处打印日志,便于记录由平台执行命令的情况,用于追溯命令下发情况。 mainInstruct 操作命令,如:Control minorInstruct 操作参数,如:SetTemperature,SetHumidness value 命令下发的参数值,如:22 */ //获取设备实际执行的结果 dynamic controlResponse = ConnClientManager.Instance.WriteValueAsync(_connectionConfig.ServerUrl, mainInstruct, value); //将执行结果对象转换成json字符串 var csResponse = JsonConvert.SerializeObject(controlResponse); //给当前设置点赋值响应内容,用于北向转发时告知设备实际执行结果 this.equipitem.curSetItem.csResponse = csResponse; //记录执行传参及响应结果到日志中,便于追溯。 string logMsg = string.Format("命令下发参数,设备号:{0},mainInstruct:{1},minorInstruct:{2},value:{3},下发执行结果:{4}", this.equipitem.iEquipno, mainInstruct, minorInstruct, value, csResponse); DataCenter.Write2Log(logMsg, LogLevel.Warn); //根据设备执行状态,返回状态,对于发布订阅模式可直接返回true,在相关地方做好日志记录即可。 if (controlResponse.Code == 200) return true; else return false; } ``` #### 如何高效采集设备数据 如何提高通讯效率,什么样的协议驱动需要做设备拆分。 某个协议通过一个服务地址,就可以将所有数据进行传输,如OPCUA,Modbus,MQTT,TCP等。以下将以OPC举例,如何高效的采集数据。下图中展示了一个OPCUA服务下的节点信息。 ![opcnode](./doc/opcnode.png) 通常,不同的节点都是来自各种各样的终端设备,如:ns=3;i=1001,ns=3;i=1002,ns=3;i=1003,ns=3;i=1004,这4个节点可能来自一个或者多个终端设备,在这里并不能看出具体的终端名称,但可能有相应的终端点位映射说明。 那么我们是否就就基于OPCUA协议插件,在代码逻辑中将设备及属性自动拆分好呢? 其实想这样一步到位也无可厚非,但这样会带来几个问题: ``` 1、配置问题,每个设备需要配置OPCUA的连接信息,连接信息修改后相关设备都需要修改。 2、性能问题,设备数量多,占用通讯线程数量,采集数据实时性下降。 ``` 对于这种场景,我们约定采用如下方案: 1、一个OPCUA连接就只建一个设备,将当前连接下的所有节点数据采集到遥测中。这样一个设备连接独享一个线程进行通讯,采集效率将大幅提升,同时也可以降低资源的消耗。 ``` 如:有一个OPC服务,采集每层楼的机房温湿度传感器数据,如红框中,温度和湿度是属于不同楼层的一个终端设备。从截图中,我们可以分成5个设备,即每个楼层一个温湿度传感器设备。 ``` ![zixitong](./doc/zixitong.png) 2、使用虚拟设备协议插件(GWVirtualEquip.STD)拆分成终端设备及属性。虚拟设备协议插件因不需要与实际设备进行通讯连接,没有连接的开销,直接从缓存字典获取OPCUA服务#1设备中拿出相应属性,采集数据非常快。 ``` 如下图所示,已将OPCUA服务#1设备中的遥测量全部拆分到每个实际的传感器设备实例中。关于使用虚拟设备协议插件使用,可以参考这个连接。 ``` ![xunishebei](./doc/xunishebei.png)