自定义驱动说明

当前HiperMATRIX EdgeStation集成一些常用的驱动以供设备连接,包括有mqtt, Siemens,opcua,allen-bradley,modbus等协议,详情查看用户手册创建连接,然而在工业领域中存在有大量的硬件,硬件设备厂商较多,所支持的驱动链接不尽相同。为此,HiperATRIX EdgeStation平台提供一种自定义驱动方式集成,用户可自我完成驱动的编写,以插件形式集成至HiperATRIX EdgeStation,以此达到扩展目的。

So Let's Begin!

1.开发环境

本文使用Java开发环境:

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 EdgeStation 的数据结构进行数据整合即可, 上层数据处理交给HiperMATRIX EdgeStation 即可。

  • 主动型驱动需要用户主动实现读写接口,实现数据接受解析并组装为HiperMATRIX EdgeStation需要的接口。

本文提供两种自定义驱动集成的案例进行实现, 用户可查看代码根据需要进行编写。

示例代码:example

4.关键注解与接口解释

@Driver

注解驱动,在驱动类上增加该注解,HiperMATRIX EdgeStation才能识别该类为驱动类,进行加载并实现。

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 EdgeStation为了实现自定义驱动实现,需要对驱动配置类进行注解标识,HiperMATRIX EdgeStation即可自动配置。

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.接口关系图表一览

image-20221023184018313

6.自定义驱动集成方式

根据上述简述驱动分类中,我们将驱动简单划分为:

  • 监听型驱动
  • 主动型驱动

进行驱动集成先明确实现该驱动属于哪种数据获取方式。

  • 若驱动为监听型驱动(例如mqtt)方式,则用户可创建自己的驱动类,同时继承ListenerConnection 抽象类,实现ListenerConntion中所有抽象方法,将获取到的数据进行解析符合为IOT层面的数据格式,之后交给ListenerConnection 父类即可,ListenerConnection 父类包含大量已经提前处理好的默认实现。

  • 若驱动为主动型驱动(例如 modbus)方式,则用户需要创建自己的驱动类,同时实现DriverConnection 接口,用户需要自行处理所有接口,完成所有相关的操作实现即可。

  • 若驱动两种方式均可实现(例如OPCUA, 可订阅,也可主动读取),则用户也是实现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会进行数据采集并存储。

image-20221023203902367

  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 EdgeStation 的驱动管理界面中进行上传驱动,等待几分钟HiperMATRIX EdgeStation 会重新启动并加载该jar包,用户可自行进行测试连接,若发生错误则自行从连接日志中进行查看。

image-20221021113052870

2024-10-14
0