当前HiperMatrix工业物联网平台集成一些常用的驱动以供设备连接,包括有mqtt, Siemens,opcua,allen-bradley,modbus等协议,详情查看用户手册中边缘连接管理,然而在工业领域中存在有大量的硬件,硬件设备厂商较多,所支持的驱动链接不尽相同。为此,HiperMatrix平台提供一种自定义驱动方式集成,用户可自我完成驱动的编写,以插件形式集成至HiperMatrix平台,以此达到扩展目的。
So Let's Begin!
1.开发环境
本文使用Java开发环境:
- 操作系统:Windows10
- JDK版本:JDK8
- 集成开发环境:IntelliJ IDEA社区版
- 使用Gradle进行项目整合
2.添加依赖
在build.gradle文件中,添加以下依赖:
// 加入基础依赖 ,当前版本使用 1.3.7-SNAPSHOT 时间:2023.6.26
compile('com.hvisions:driver-base:1.3.7-SNAPSHOT') {
changing = true
}
// lombok 依赖
compileOnly 'org.projectlombok:lombok:1.18.12'
annotationProcessor 'org.projectlombok:lombok:1.18.12'
testCompileOnly 'org.projectlombok:lombok:1.18.12'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.12'
// Use JUnit test framework
testImplementation 'junit:junit:4.13'
3.简述驱动分类
协议驱动传输方式各种各样,但我们可以根据驱动获取数据方式,
- 主动发送请求以获取数据 (例如西门子siemens设备, modbus 数据传输)
- 服务器进行推送数据(例如mqtt, websocket 协议等)
将驱动简单的划分为两种:
- 监听型驱动
- 主动型驱动
两种驱动方式有不同的数据处理方式,上层处理均为相同的接口,不过监听型驱动有些默认实现
-
监听型驱动用户只需打开连接持续监听服务器,在获取到数据时使用回调函数进行数据解析,按照HiperMatrix 的数据结构进行数据整合即可, 上层数据处理交给HiperMatrix 即可。
-
主动型驱动需要用户主动实现读写接口,实现数据接受解析并组装为HiperMatrix需要的接口。
本文提供两种自定义驱动集成的案例进行实现, 用户可查看代码根据需要进行编写。
示例代码:example
4.关键注解与接口解释
@Driver
注解驱动,在驱动类上增加该注解,HiperMatrix 才能识别该类为驱动类,进行加载并实现。
package com.hvisions.iot.drivers.base.annotation;
import com.hvisions.iot.drivers.base.connection.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Driver {
String name();
String desc();
Model[] models();
ProtocolType protocol();
DriverType type();
AccessType accessType();
Class<?> driverConfig() default BaseConnectionConfig.class;
boolean supportBrowse() default false;
boolean enable() default true;
String author() default "hvisions";
String version() default "";
String publishDateTime() default "";
}
字段解释
注解属性 | 解释 | 备注 |
---|---|---|
String name(); | 驱动名称 | 唯一不可重复,作为驱动的标识 |
String desc(); | 驱动描述 | 简述驱动的作用 |
Model[] models(); | 可支持的模块列表 | 驱动可支持的模块类型,例如西门子驱动,旗下包含大量s7-200,s7-300, s7-1200, s7-1500 等不同的模块,需要进行区分。亦或多种模块为同一实现方式,也可使用该注解进行解释。详情查看@Model 注解 |
ProtocolType protocol(); | 协议类型 | 自定义协议请指定为:com.hvisions.iot.drivers.base.connection.ProtocolType.CUSTOMER |
DriverType type(); | 驱动类型 | 暂时无用,可根据驱动类型自行指定 |
AccessType accessType(); | 访问方式 | - LISTENER 监听型驱动 - CALLER 主动型驱动 请根据驱动类型选择 |
Class<?> driverConfig() default BaseConnectionConfig.class; | 驱动配置 | 指定驱动所需要的配置类,含有默认配置类BaseConnectionConfig 对于配置类中的注解查看 @FieldConfig 注解 |
boolean supportBrowse() default false; | 是否支持浏览 | 对于一些驱动支持点位浏览的功能。例如opcua,可直接进行请求点位配置。若驱动含有该功能,还需要实现接口实现com.hvisions.iot.drivers.base.connection.DriverReader#browse函数。 |
boolean enable() default true; | 是否启用 | 驱动是否被加载,可通过驱动管理进行调整 |
String author() default "hvisions"; | 驱动作者 | 用户自定义实现, |
String version() default ""; | 驱动版本 | 用户自定义实现,亦或者通过maven/gralde 脚本实现打包时生成driverVersion.properties,其中 生成如下信息,则加载时会动态替换 #Thu Sep 01 11:20:40 CST 2022 info.build.publishTime=2022-09-01 11:20:40 info.build.version=1.2.10-SNAPSHOT-driver-simulator |
String publishDateTime() default ""; | 驱动发布时间 | 用户自定义实现,亦或者通过maven/gralde 脚本实现打包时生成driverVersion.properties,其中 生成如下信息,则加载时会动态替换 #Thu Sep 01 11:20:40 CST 2022 info.build.publishTime=2022-09-01 11:20:40 info.build.version=1.2.10-SNAPSHOT-driver-simulator |
@Model
模块注解,进行驱动的实现描述
package com.hvisions.iot.drivers.base.annotation;
import com.hvisions.iot.drivers.base.connection.ModelCategory;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Model {
String name();
String label() default "";
String desc() default "";
ModelCategory category();
}
字段解释
注解属性 | 解释 | 备注 |
---|---|---|
String name(); | 模块名称 | 请确保唯一,可通过BaseConnectionConfig.model属性进行获取,作为实现的唯一标识,连接根据此属性进行创建 |
String label() default ""; | 模块标签 | 模块的展示,用于在连接中查看显示 |
String desc() default ""; | 模块描述 | 增加模块的解释信息 |
ModelCategory category(); | 协议分类 | 对于自定义协议,作为驱动的分类,请使用com.hvisions.iot.drivers.base.connection.ModelCategory.CUSTOMER 作为唯一实现 |
@FieldConfig
属性配置注解, HiperMatrix 为了实现自定义驱动实现,需要对驱动配置类进行注解标识,HiperMatrix即可自动配置。
package com.hvisions.iot.drivers.base.annotation;
import com.hvisions.iot.drivers.base.connection.ConfigDataType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 属性注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldConfig {
String label();
String description() default "";
String initialValue() default "";
boolean required() default false;
boolean display() default true;
int order() default -1;
ConfigDataType configDataType() default ConfigDataType.TEXT;
boolean isArray() default false;
String group() default "";
/**
* 同组条件
*/
String condition() default "";
}
注解属性 | 解释 | 备注 |
---|---|---|
String label(); | 属性在配置时的展示 | |
String description() default ""; | 属性的额外描述信息 | |
String initialValue() default ""; | 属性的初始值,对于boolean或数字类型,请直接使用字符串包裹即可,例如 initialValue = "true" | |
boolean required() default false; | 是否必填 | |
boolean display() default true; | 是否展示 | |
int order() default -1; | 配置的顺序 | |
ConfigDataType configDataType() default ConfigDataType.TEXT; | 配置展示方式 | 默认使用文本输入框,可根据需要自行选择 |
boolean isArray() default false; | 是否为数组集合 | |
String group() default ""; | 同组配置 | 配合condition使用 |
String condition() default ""; | 同组条件 | 当多个属性为同组属性时,为关键属性设置条件,直接使用字符串包裹即可,当满足条件则同组配置展示 |
DriverReader
驱动读取接口
函数方法 | 简介 | 入参 | 返回值 |
---|---|---|---|
ReadResponse read(ReadRequest request); | 同步读取属性值 | 请求的内容 | 读返回值 |
List<AlarmData> readAlarm() | 同步读取报警信息,默认Null | 返回告警数据列表 | |
CompletableFuture<ReadResponse> asyncRead(ReadRequest request) | 异步读取属性值 | 请求的内容 | 读返回值 |
boolean supportSubscribe() | 是否支持订阅 | true: 支持, false: 不支持 | |
SubscriptionResponse subscribe(SubscriptionRequest request, Consumer<DataValue> notification) | 订阅属性值 | @param request 订阅请求内容 @param notification 订阅通知函数 |
订阅的ID号 |
List<FieldNode> browse() | 列出属性结构树,比如opcua协议就可以支持该方法 | FieldNode结构树 | |
void setDataCallback(Consumer<FieldData> dataCallback) | 设置数据回调函数,一般对于MQTT,文件方式需要数据回调的接口方式有效 | 数据回调函数 | |
void setAlarmCallback(Consumer<AlarmData> alarmCallback) | 设置告警回调函数,一般对于MQTT,文件方式需要数据回调的接口方式有效 | 告警回调函数 |
package com.hvisions.iot.drivers.base.connection;
import com.hvisions.iot.drivers.base.message.*;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
public interface DriverReader {
/**
* 同步读取属性值
*
* @param request 请求的内容
* @return ReadResponse对象
*/
ReadResponse read(ReadRequest request);
/**
* 同步读取报警信息,默认Null
* @return
*/
default List<AlarmData> readAlarm() {
return null;
}
/**
* 异步读取属性值
*
* @param request 请求的内容
* @return ReadResponse对象
*/
CompletableFuture<ReadResponse> asyncRead(ReadRequest request);
/**
* 是否支持订阅
*
* @return true: 支持, false: 不支持
*/
default boolean supportSubscribe() {
return false;
}
/**
* 订阅属性值
*
* @param request 订阅请求内容
* @param notification 订阅通知函数
* @return 订阅的ID号
*/
default SubscriptionResponse subscribe(SubscriptionRequest request, Consumer<DataValue> notification) {
return null;
}
/**
* 列出属性结构树,比如opcua协议就可以支持该方法
*
* @return FieldNode结构树
*/
default List<FieldNode> browse() {
return Collections.emptyList();
}
/**
* 设置数据回调函数,一般对于MQTT,文件方式需要数据回调的接口方式有效
*
* @param dataCallback 数据回调函数
*/
default void setDataCallback(Consumer<FieldData> dataCallback) {
// 默认不做任何设置
}
/**
* 设置告警回调函数,一般对于MQTT,文件方式需要数据回调的接口方式有效
*
* @param alarmCallback 告警回调函数
*/
default void setAlarmCallback(Consumer<AlarmData> alarmCallback) {
// 默认不做任何设置
}
}
DriverWriter
驱动写入接口
函数方法 | 简介 | 入参 | 返回值 |
---|---|---|---|
WriteResponse write(WriteRequest request) | 写入属性值 | request 写入请求 | WriteResponse对象 |
package com.hvisions.iot.drivers.base.connection;
import com.hvisions.iot.drivers.base.message.WriteRequest;
import com.hvisions.iot.drivers.base.message.WriteResponse;
public interface DriverWriter {
/**
* 写入属性值
*
* @param request 写入请求
* @return WriteResponse对象
*/
WriteResponse write(WriteRequest request);
}
DriverConnection
==对于主动型驱动请重点实现该接口==
驱动连接接口, 组装了读写接口与连接接口, 实现该接口即可。
函数方法 | 简介 | 入参 | 返回值 |
---|---|---|---|
static boolean isCluster(JsonNode settings) | 判断driver是否是集群模式 | driver配置信息 | true or false |
static List<JsonNode> splitClusterSetting | 解析集群配置 | driver配置信息 | driver配置信息列表 |
void connect(); | 连接服务 | ||
boolean isConnected(); | 是否已经连接成功 | true: 连接成功 false: 未连接成功 | |
void reconnect() | 重连操作 | ||
void close(); | 关闭连接 | ||
ThingField buildField(String name, String origin, DataType type, Boolean unsigned, Integer arrayLength, String equipmentId, ThingFieldObject thingFieldObject) | 生成ThingField对象 | name – 属性名称 origin – 属性地址 type – 属性数据类型 unsigned – 是否为无符号 arrayLength – 数组长度,属性是数组是有效 |
ThingField对象, 如果origin为非法的地址,返回为空 |
boolean isFieldValid(String origin) | 属性定义是否是合法的地址 | origin – ThingField地址定义 | ture: 合法的地址, false: 不合法的地址 |
void setConnectEventCallback(Consumer<Boolean> statusListener) | 设置状态通知函数,当连接状态发生变化时,调用statusListener,true表示连接成功,false表示连接失败 | statusListener – 状态通知函数 | |
ConnOverview overviewInfo() | 获取 网关的概览信息 |
package com.hvisions.iot.drivers.base.connection;
import com.fasterxml.jackson.databind.JsonNode;
import com.hvisions.iot.drivers.base.message.DataType;
import com.hvisions.iot.drivers.base.message.ThingField;
import com.hvisions.iot.drivers.base.message.impl.DefaultThingField;
import com.hvisions.iot.drivers.base.message.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
public interface DriverConnection
extends DriverReader, DriverWriter {
/**
* 判断driver是否是集群模式,不要改变isCluster这个名称
*
* @param settings driver配置信息
* @return true or false
*/
public static boolean isCluster(JsonNode settings) {
return false;
}
/**
*
* @param settings driver配置信息
* @return driver配置信息列表
*/
public static List<JsonNode> splitClusterSetting(JsonNode settings) {
return Collections.singletonList(settings);
}
/**
* 连接服务
*/
void connect();
/**
* 是否已经连接成功
*
* @return true: 连接成功 false: 未连接成功
*/
boolean isConnected();
default void reconnect() {
close();
connect();
}
/**
* 关闭连接
*/
void close();
/**
* 生成ThingField对象
*
* @param name 属性名称
* @param origin 属性地址
* @param type 属性数据类型
* @param unsigned 是否为无符号
* @param arrayLength 数组长度,属性是数组是有效
* @param equipmentId 设备id
* @param thingFieldObject 点位对象定义信息
* @return ThingField对象, 如果origin为非法的地址,返回为空
*/
default ThingField buildField(String name, String origin, DataType type, Boolean unsigned,
Integer arrayLength, String equipmentId, ThingFieldObject thingFieldObject) {
return DefaultThingField.build(name, origin, type, unsigned, arrayLength, equipmentId, thingFieldObject);
}
/**
* 属性定义是否是合法的地址
*
* @param origin ThingField地址定义
* @return ture: 合法的地址, false: 不合法的地址
*/
default boolean isFieldValid(String origin) {
return true;
}
/**
* 设置状态通知函数,当连接状态发生变化时,调用statusListener,true表示连接成功,false表示连接失败
*
* @param statusListener 状态通知函数
*/
default void setConnectEventCallback(Consumer<Boolean> statusListener) {
}
/**
* 获取 网关的概览信息
* @return
*/
default ConnOverview overviewInfo() {
return null;
}
}
ListenerConnection
对于监听型驱动请重点实现该驱动
监听型驱动的抽象类,内部含有HiperMatrix 的默认实现以接入数据,用户进行实现即可。
5.接口关系图表一览
6.自定义驱动集成方式
根据上述简述驱动分类中,我们将驱动简单划分为:
- 监听型驱动
- 主动型驱动
进行驱动集成先明确实现该驱动属于哪种数据获取方式。
-
若驱动为监听型驱动(例如mqtt)方式,则用户可创建自己的驱动类,同时继承ListenerConnection 抽象类,实现ListenerConntion中所有抽象方法,将获取到的数据进行解析符合为IOT层面的数据格式,之后交给ListenerConnection 父类即可,ListenerConnection 父类包含大量已经提前处理好的默认实现。
-
若驱动为主动型驱动(例如 modbus)方式,则用户需要创建自己的驱动类,同时实现DriverConnection 接口,用户需要自行处理所有接口,完成所有相关的操作实现即可。
-
若驱动两种方式均可实现(例如OPCU, 可订阅,也可主动读取),则用户也是实现DriverConnection 接口。同时覆盖其中的所有实现。包括其中的默认实现方式,可重新覆盖以完善自定义驱动逻辑处理。
将驱动实现编写完成之后,需要在该实现类上增加**@Driver注解,注明其中的说明方式,详细说明请查看上层@Driver**注解解释说明。
若驱动包含配置初始化类,请务必覆盖 driverConfig 注解项,该项为驱动所需要的配置类,若不实现,则使用默认的配置。造成参数缺失,初始化失败。
同时编写该驱动所需要的配置类,请默认继承 BaseConnectionConfig 类,该类中包含基础的配置项,同时编写驱动所需要的配置项,使用 @FieldConfig 进行注解标明,则IOT在注册发现驱动时会进行扫描解析所有的配置项,前端会按照配置自动生成对应的配置界面。
用户可自行完成大量复杂的配置,保证配置类的最终父类继承BaseConnectionConfig类即可,IOT进行扫描时会进行父类的迭代处理,包含该类与父类的完整配置。
在进行编写完成之后。用户需要为驱动实现类创建唯一的有参构造, 其中包括驱动的配置类,IOT在创建连接时会进行构造器发现,获取唯一的有参构造,然后进行参数解析传递,实现驱动类的实例初始化。
用户在进行唯一有参构造时,可传递大量参数,包括 驱动配置类, 线程池, 定时器, Log日志。 IOT在初始化时会进行参数解析并复制。
注意: 若想要将驱动的连接日志进行额外捕获处理,则需要将Log日志参数进行传递。IOT上层会进行log解析,同时使用EFK进行日志采集处理。方便用户快速定位驱动连接日志问题。
7.IOT驱动加载原理解释
iot使用注解扫描方式进行发现驱动实现,通过驱动实现类的唯一有参构造提供实例化,具体实现方式为:
(1)进行指定包(com.)扫描发现获取所有带有@Driver注解的驱动类。
(2)进行驱动类解析。
- 包括驱动的名称,显示,可支持模块,类别,协议,类型,参数配置类,作者,版本,发布信息等基础信息,以供前端提供自适应配置。
- 进行驱动类的构造器扫描,获取唯一的带参构造器。
- 进行国际化扫描,根据驱动所提供的 i18n Resource Bundle 文件进行扫描,解析并获取国际化支持。
(3)当用户进行创建初始化连接时,IOT会进行参数解析,并调用驱动实现类的唯一有参构造进行初始化,此时驱动所需要的连接参数均会传递给实现的驱动类。
(4)当后续进行操作时,均为DriverConnection 接口中规定的所有方式。请自行实现,相关详细说明请查看上述DriverConnection 接口说明。
(5)同时IOT提供了驱动的重连机制,IOT内部提供有后台线程进行检测驱动连接状态,会将所有连接失败的连接每30秒进行重连服务,确保连接的重连恢复机制,在重连中开放了连接回调函数,用户可自行实现重连后的操作。
(6)同时驱动提供了点位异常属性的冻结处理,(递进算法处理,确保点位的恢复性)当某些属性因各种原因发生故障没有理想值时,IOT内部做出冻结处理,防止过多的异常点位影响整个驱动的响应速度。
8.数采方案实现的整体思路
一句话简述数采的整体过程
对于数采的理解,其实就是从不同的数据源连接处获取数据,根据一定的规则(周期性采集或值改变方式)进行读取并存储时序数据,同时包括对于源数据的计算处理过滤加工等功能。
术语解释
术语 | 释义 | 备注 |
---|---|---|
驱动 | 驱动其实就是各种数据源来源的实现方式。 | 例如对于西门子设备数据的读取,就需要实现Siemens 系列驱动,根据一定的配置以完成读取西门子设备数据。 同时适用于 mqtt, opcua 等,都属于驱动范畴。 |
连接 | 连接是根据驱动类型与配置创建的连接实体。 | 例如实现了mqtt的驱动方式,即可根据不同的ip,端口创建出多个不同的mqtt连接,从而访问数据。 |
设备 | 设备是一组点位的集合。(可以是真实的物理设备,也可以是逻辑范围内的设备实体) | 例如,对于mqtt连接,根据不同的topic获取到不同的数据,可通过设备集合将其统一处理。 |
属性 | 属性是点位的别称。是IOT进行点位读取与存储点位数据点位的最小单位。 | 通过对于点位的不同配置,IOT会从连接中获取数据并封装给属性值,同时进行计算,过滤等处理 |
物模型 | 物模型是一组标准化属性点位的抽象处理 | 在创建设备属性时,往往有大量相似点位的创建,这是因为设别的共通性,极其繁琐又是必须工序。因此可创建物模型以提前规范属性集合,再创建设备时继承物模型即可。(在设备中对于继承物模型的属性不可删除,只可更改) |
9.IOT平台运行原理说明
用户使用IOT平台,可创建响应的数据源连接。同时创建设备,属性等,IOT会进行数据采集并存储。
(1)用户进行创建连接,指定连接类型,驱动模块,填写相应配置,即可创建出连接实例。
(2)用户创建物模型,物模型即为提前规定好的一组标准化属性。
(3)用户创建设备,可指定物模型,指定连接,可指定多个连接。
(4)用户创建属性,若在多连接设备中指定对应的连接即可从相应连接中获取数据。
10.对于IOT数采点位的处理
IOT将点位以属性类别进行划分:
- 静态属性(基本属于用户标识点位,不参与IOT内部流程处理 )
- 动态属性(再根据采集方式决定IOT以何种采集方式进行采集)
- 计算属性(根据用户配置表达式进行表达式运算)
对于动态属性中,采集方式分为两种:
(1)周期采集
- 周期采集中,根据用户指定的循环周期进行周期性异步读取,将读取结果进行统一封装并发送至MQ,后经由data-storage-service进行数据存储处理。
- 同时,因为周期采集依靠驱动能力进行周期调度,若驱动能力较弱处理不来,上层会进行请求抛弃,以降低系统压力,同时发出警告,可能会造成漏值情况。
(2)值改变
- 值改变,见名知意,当值进行改变之后进行存储。
- 在IOT中,若驱动支持值改变订阅功能(例如opcua),则进行值改变即为订阅功能,将数据进行订阅处理,若数据发生改变则进行推送处理
- 若驱动并不支持值改变订阅功能(例如mqtt,虽属于订阅推送数据,但并不能保证推送数据为变化数据),则IOT内部进行建立订阅线程扫描,根据用户配置的扫描周期进行周期扫描并比对,实现自定义的值改变推送能力。
对于计算属性,IOT支持表达式计算功能,会根据用户配置的表达式中进行变量获取并计算,同时根据采集方式进行存储。
11.属性详解补充
属性是IOT进行点位读取与存储点位数据点位的最小单位。
属性关键字段解释
字段 | 释义 | 备注 |
---|---|---|
lable | 属性名称 | 唯一 |
name | 属性标识 | 唯一,作为点位存储的字段名称 |
desc | 属性描述 | |
connectorId | 连接ID | 当设备为多练接时,属性可指定连接 |
category | 属性类别 | 根据属性获取数据方式划分为三种: 1. 动态属性,通过驱动获取值 2. 静态属性, 用于自行指定静态值 3. 计算属性,通过运算表达式进行计算得出的结算结果 |
type | 属性类型 | 属性值的数据类型 |
origin | 属性地址 | 驱动可根据地址作为数据标识 |
mode | 采集方式 | 划分为两种: 1. 周期采集,IOT根据配置的扫描间隔进行周期采集。 2. 值改变,IOT进行属性点位的订阅处理,当属性值进行改变时进行数据上传处理(部分驱动不支持订阅,IOT内部实现了内部循环订阅方式,进行内部扫描值进行订阅处理。) |
intervals | 扫描周期 | IOT根据配置的扫描间隔进行周期采集 |
isArray | 是否为数组 | |
writable | 是否可写 | true则可以进行调用写入接口,实现驱动的写入值功能 |
expression | 表达式 | 用于计算的表达式功能 |
max | 最大值 | 用于属性值过滤 |
min | 最小值 | 用于属性值过滤 |
12.结语
将代码书写完成,使用gradle打包工具,将此项目打包成为jar包,然后进行HiperMatrix 的驱动管理界面中进行上传驱动,等待几分钟HiperMatrix 会重新启动并加载该jar包,用户可自行进行测试连接,若发生错误则自行从连接日志中进行查看。