# 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
### 介绍
协议插件开发示例代码
### 软件架构

由软件架构示意图可知,系统会在调用init初始化成功之后调用GetData更新数据点位值。
SetParm可对点位值进行操作更新。
### 安装教程
将生成的文件复制到软件目录的dll下

### 使用说明
该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服务下的节点信息。

通常,不同的节点都是来自各种各样的终端设备,如: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个设备,即每个楼层一个温湿度传感器设备。
```

2、使用虚拟设备协议插件(GWVirtualEquip.STD)拆分成终端设备及属性。虚拟设备协议插件因不需要与实际设备进行通讯连接,没有连接的开销,直接从缓存字典获取OPCUA服务#1设备中拿出相应属性,采集数据非常快。
```
如下图所示,已将OPCUA服务#1设备中的遥测量全部拆分到每个实际的传感器设备实例中。关于使用虚拟设备协议插件使用,可以参考这个连接。
```
