# GWDriverDemo_Java **Repository Path**: ganweicloud/gwdriver-demo_java ## Basic Information - **Project Name**: GWDriverDemo_Java - **Description**: GWDriverDemo.STD的Java语言开发版本。 协议插件开发示例代码,介绍了如何开发一个协议插件,继承框架的CEquipBase类后,重写基类中的几个方法来实现当前协议的内在逻辑。其中Init负责传入设备所需的连接参数,GetData负责一个周期内获取获取设备数据,GetYC和GetYX负责对获取到的数据进行赋值。 - **Primary Language**: Unknown - **License**: LGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-03-31 - **Last Updated**: 2025-03-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # GWDriverDemo.STD ### 介绍 协议插件开发示例代码 ### 软件架构 ![img.png](./doc/img.png) 由软件架构示意图可知,系统会在调用init初始化成功之后调用GetData更新数据点位值。 SetParm可对点位值进行操作更新。 ### 安装教程 将生成的文件复制到软件目录的dll下 ![img.png](./doc/img-1.png) ### 运行 1、目录层级 ``` +---bin | | gwminidatacenter-1.0.jar | \---lib | comm-1.0.0.jar | jackson-annotations-2.13.5.jar | jackson-core-2.13.5.jar | jackson-databind-2.13.5.jar | jackson-dataformat-xml-2.13.5.jar | jackson-datatype-jdk8-2.13.5.jar | jackson-datatype-jsr310-2.13.5.jar | org.eclipse.paho.client.mqttv3-1.2.5.jar | stax2-api-4.2.1.jar | woodstox-core-6.4.0.jar | +---dll | GWDriverDemo-1.0.jar | \---log XLog.txt ``` 2、运行命令 ``` java -jar gwminidatacenter-1.0.jar ``` **注:一般情况下需要使用环境变量,此时可使用set(windows)或者export(Linux)先初始化后,再调用运行命令** ``` set InstanceId=20250304 ``` ### 使用说明 该demo使用随机值进行实际场景的更新。 ##### 初始化设备相关参数 ```java /** 设备通信间隔时间 */ private int _sleepInterval = 0; /** 设备连接参数信息,如果连接参数很简单,可以不用定义对象。 */ private ConnectionConfig _connectionConfig; /** 当前设备的实时数据 */ private HashMap _currentValue; /** 当前设备的实时事件,开发者需要处理相同事件内容重复产生的问题。 */ private ArrayList _currentEvents; /** 自定义当前设备的事件名称级别 如果有多个事件名称需要定义,可以在自定义参数中进行定义 */ private int _defaultEventMessageLevel = 0; /** 初始化设备相关参数 在界面添加完成后,会进入到该方法进行初始化 之后再界面修改连接参数后,会再一次进入该方法。 @param item equip表对象属性 @return */ @Override @Override public boolean 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格式来存储这个数据。 */ //获取设备连接通讯的间隔时间。 _sleepInterval = Integer.parseInt(item.communication_time_param); /* 在构造连接参数数,根据实际情况,以下展示一个连接参数模型的赋值。 如果连接参数简单,也可以使用自定义连接参数,直接使用communication_param更好,减少配置项,这里需要开发人员自己确定好。 */ if (!StringHelper.isNullOrWhiteSpace(item.Reserve2)) { ObjectMapper mapper = new ObjectMapper(); try { JsonNode dictParams = mapper.readTree(item.Reserve2); _connectionConfig = new ConnectionConfig(); _connectionConfig.setServerUrl(item.getEquipAddr()); _connectionConfig.setUserName(dictParams.path("UserName").toString()); _connectionConfig.setPassword(dictParams.path("Password").toString()); _connectionConfig.setCertificatePath(dictParams.path("CertificatePath").toString()); _connectionConfig.setCertificatePwd(dictParams.path("CertificatePwd").toString()); //我们可以定义多个事件名称的级别,命名方式如DefaultEventMessageLevel,如果未取到,默认值给0,但最好要区分好,因为使用0的事件级别很多场景都使用。 String messageLevel = dictParams.path("DefaultEventMessageLevel").toString(); _defaultEventMessageLevel = StringHelper.isNullOrEmpty(messageLevel) ? 0 : Integer.parseInt(messageLevel); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } return super.init(item); } ``` ##### 建立设备连接 ```java /** 设备连接初始化 对于设备的连接地址,连接账号密码发生更改后,可以进行重连。 @return */ @Override public boolean OnLoaded() { //TODO 这里可以写于设备连接的具体代码了。根据_connectionConfig连接参数,去创建自己的连接对象。 ConnClientManager.getInstance().CreateClientSession(_connectionConfig); //返回默认值 return super.OnLoaded(); } ``` ##### 获取设备状态及实时数据 ```java /** 获取设备状态及实时数据 注意要控制好该方法不要出异常,否则会出现设备一直处于初始化状态中 @param pEquip 设备基类对象 @return */ @Override public CommunicationState GetData(CEquipBase pEquip) { //通过等待间隔时间,来达到多久取一次的。 if (_sleepInterval > 0) { super.Sleep(_sleepInterval); } //当然开发者也可以在此次在增加相关业务逻辑。 //获取当前连接地址的状态 var equipStatus = ConnClientManager.getInstance().GetClientSessionStatus(_connectionConfig.getServerUrl()); //如果连接状态正常,设置为在线 if (equipStatus) { //只有在线是才采集数据 _currentValue = ConnClientManager.getInstance().GetCurrentValues(_connectionConfig.getServerUrl(), pEquip.getMEquipNo()); //如果设备在线,默认执行基类的方法,base.GetData(pEquip)返回值是ok。 return super.GetData(pEquip); } else { //否则设置离线 return CommunicationState.fail; } } ``` ##### 遥测点数据更新 ```java /** 遥测点设置 @param r ycp表对象属性(不是全部) @return */ @Override public boolean 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); 1.当无法取到数据时,但设备连接时正常。需要遵循一些约束 默认设置遥测值为:*** 2.当部分遥测数据正常,一部分不正常时。 开发者也可以产生"数据不完整”的事件内容,该内容可以通过北向转发到应用侧。 但需要注意该类事件产生的频率。 */ /* 实时数据示例代码,可以根据自己的业务进行处理*/ if (_currentValue == null) { return true; } try { //此处的Key值需要根据实际情况去处理。如果构造实时数据缓存字典是需要由开发去定义。 //总的来说,按照设备+遥测遥信的方式构造缓存数据是比较合理的。 String key = r.getEquipNo() + "_" + r.getMainInstruction(); if (_currentValue.containsKey(key)) { var objValue = _currentValue.get(key); if (objValue == null) { SetYCData(r, "***"); //此处不可以设置为null。 } else { SetYCData(r, objValue); } } else { SetYCData(r, "***"); } } catch (RuntimeException ex) { SetYCData(r, "测点赋值出现异常,请查看日志"); DataCenter.WriteLogFile(String.format("记录报错日志:%1$s", ex)); } //此处默认都返回true,否则设备会处于离线。 return true; } ``` ##### 遥信点数据更新 ```java /** 遥信点设置 @param r yxp表对象属性(不是全部) @return */ @Override public boolean 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); 1.当无法取到数据时,但设备连接时正常。需要遵循一些约束 默认设置遥信值为:*** 2.当部分遥测数据正常,一部分不正常时。 开发者也可以产生"数据不完整”的事件内容,该内容可以通过北向转发到应用侧。 但需要注意该类事件产生的频率。 */ /* 实时数据示例代码,可以根据自己的业务进行处理*/ if (_currentValue == null) { return false; } try { String key = r.getEquipNo() + "_" + r.getMainInstruction(); if (_currentValue.containsKey(key)) { var nodeIdObj = _currentValue.get(key); if (nodeIdObj == null) { SetYXData(r, "***"); } else { SetYXData(r, nodeIdObj); } } else { SetYXData(r, "***"); } } catch (RuntimeException ex) { SetYXData(r, "遥信赋值出现异常,请查看日志"); DataCenter.WriteLogFile(String.format("记录报错日志:%1$s", ex)); } return true; } ``` ##### 设备事件发布 ```java /** 事件发布 如门禁设备的一些通行记录数据。 如果对事件记录实时性有非常高的要求,可以接收到事件后直接转。 @return */ @Override public boolean GetEvent() { //从当前设备连接中获取事件列表 _currentEvents = ConnClientManager.getInstance().GetCurrentEvents(_connectionConfig.getServerUrl(), this.getMEquipNo()); if (_currentEvents == null) { return true; } //假设_currentEvents对象每次都是新的数据,不存在旧数据,需开发者自行处理好. for (var eventItem : _currentEvents) { //EquipEvent中的事件级别根据当前事件名称定义好的级别。便于北向上报数据时的甄别。 EquipEvent evt = null; try { evt = new EquipEvent(new ObjectMapper().writeValueAsString(eventItem), "可以自定义的消息格式", MessageLevel.forValue(_defaultEventMessageLevel), LocalDateTime.now()); } catch (JsonProcessingException e) { DataCenter.WriteLogFile("serialization eventItem error" + e.getMessage()); continue; } super.getEquipEventList().add(evt); } _currentEvents = null; //循环完成后,将事件记录置空,避免下次重复产生相同的事件. return true; } ``` ##### 设备命令下发 ```java /** 设备命令下发 @param mainInstruct 操作命令 @param minorInstruct 操作参数 @param value 传入的值 @return */ @Override public boolean SetParm(String mainInstruct, String minorInstruct, String value) { /* 注意:建议在此处打印日志,便于记录由平台执行命令的情况,用于追溯命令下发情况。 mainInstruct 操作命令,如:Control minorInstruct 操作参数,如:SetTemperature,SetHumidness value 命令下发的参数值,如:22 */ try { //获取设备实际执行的结果 var controlResponse = ConnClientManager.getInstance().WriteValueAsync(_connectionConfig.getServerUrl(), mainInstruct, value); //将执行结果对象转换成json字符串 String csResponse = new ObjectMapper().writeValueAsString(controlResponse); //给当前设置点赋值响应内容,用于北向转发时告知设备实际执行结果 this.getEquipitem().getCurSetItem().setCsResponse(csResponse); //记录执行传参及响应结果到日志中,便于追溯。 String logMsg = String.format("命令下发参数,设备号:%1$s,mainInstruct:%2$s,minorInstruct:%3$s,value:%4$s,下发执行结果:%5$s", this.getEquipitem().getIEquipno(), mainInstruct, minorInstruct, value, csResponse); DataCenter.WriteLogFile(logMsg); //根据设备执行状态,返回状态,对于发布订阅模式可直接返回true,在相关地方做好日志记录即可。 if ((int)controlResponse.get("Count") == 200) { return true; } return false; } catch (JsonProcessingException e) { throw new RuntimeException(e); } } ``` #### 如何高效采集设备数据 如何提高通讯效率,什么样的协议驱动需要做设备拆分。 某个协议通过一个服务地址,就可以将所有数据进行传输,如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)