自定义驱动示例

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

So Let's Begin!

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

示例代码:example

1.开发环境

本文使用Java开发环境:

2.添加依赖

在build.gradle文件中,添加以下依赖:

    // 加入基础依赖 ,当前版本使用 1.3.7-SNAPSHOT 时间:2023.06.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.示例代码

关键两个类:

  • SimulatorConnection 驱动的主要实现方式
  • SimulatorSetting 驱动的相关设置
package com.hvisions.iot.drivers.simulator;

import com.hvisions.iot.drivers.base.annotation.Driver;
import com.hvisions.iot.drivers.base.annotation.Model;
import com.hvisions.iot.drivers.base.connection.*;
import com.hvisions.iot.drivers.base.logger.ConnFailCategory;
import com.hvisions.iot.drivers.base.logger.Logger;
import com.hvisions.iot.drivers.base.message.*;
import com.hvisions.iot.drivers.base.message.impl.DefaultReadResponse;
import com.hvisions.iot.drivers.base.message.impl.DefaultThingField;
import com.hvisions.iot.drivers.base.message.impl.DefaultWriteResponse;
import com.hvisions.iot.utils.base.data.DataType;
import org.apache.commons.lang3.StringUtils;


import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;

/**
 * <p>Title: SimpleEx</p>
 * <p>Description: </p>
 * <p>Company: www.h-visions.com</p>
 * <p>create date: 2021/11/1</p>
 * 
 * 
 * 	此注解非常关键,hiperMatric平台根据此注解扫描并将该类加入驱动程序中,注意name不能重复。
		目前已有name包括: HTTP_CLIENT,AbLibTag,OPC_UA,SIEMENS_PLC,MQTT_CLIENT
	其中models决定该程序可读取的驱动描述,可支持多种描述.
		name 		具体驱动的唯一标识
		label 		前端页面展示的标签
		desc  		前端页面展示的驱动面熟信息
		category 	指定的包含目录
    protocol: 		表明该程序所属的协议类型
    type:	  		标识该程序的所属类别
    accessType 		驱动使用方式 
 *
 * @author : xhjing
 * @version :1.0.0
 */
@Driver(name = "SIMULATOR",
        desc = "简单的模拟数据,无需提供地址,根据数据类型生成随机数",
        models = {
                @Model(name = "SIMULATOR", label = "SIMULATOR", desc = "产生模拟数据驱动", category = ModelCategory.SIMULATOR)
        },
        protocol = ProtocolType.SIMULATOR,
        type = DriverType.EQUIPMENT,
        accessType = AccessType.CALLER
)
public class SimulatorConnection implements DriverConnection {

    private final BaseConnectionConfig setting;

    private final Logger log;

    private final ExecutorService executorService;

    // 地址与 generator 的对照
    private Map<String, Generator> fieldGeneratorMap = new ConcurrentHashMap<>();

    private final DriverStatus driverStatus;

    public SimulatorConnection(BaseConnectionConfig setting, Logger log, ExecutorService executorService, DriverStatus driverStatus) {
        this.setting = setting;
        this.log = log;
        this.executorService = executorService;
        this.driverStatus = driverStatus;
    }

    @Override
    public void connect() {
        driverStatus.success();
        log.info("open simulator {}!", setting.getName());
    }

    @Override
    public DriverStatus isConnected() {
        return driverStatus;
    }

    @Override
    public void close() {
        driverStatus.close();
        log.info("close simulator {}!", setting.getName());
    }

    @Override
    public ThingField buildField(String name, String origin, DataType type, Boolean unsigned,
                                 Integer arrayLength, String equipmentId, ThingFieldObject thingFieldObject) {
        if (StringUtils.isBlank(origin)) {
            return null;
        }
        ThingField thingField = DefaultThingField.build(name, origin, type, unsigned, arrayLength, equipmentId, thingFieldObject);

        Generator generator = fieldGeneratorMap.get(origin);
        if (generator == null) {
            generator = buildGenerator(thingField);
            fieldGeneratorMap.put(origin, generator);
        }

        return thingField;
    }

    @Override
    public ReadResponse read(ReadRequest readRequest) {
        DefaultReadResponse readResponse = new DefaultReadResponse();
        for (ThingField requestField : readRequest.getRequestFields()) {
            if (requestField.getName().equals("error")) {
                readResponse.fail(requestField, ResponseCode.INVALID_DATA);
                log.error(ConnFailCategory.REQUEST_ERROR, "Device field read error, {}", ThingField.info(requestField));
                continue;
            }
            final Generator generator = fieldGeneratorMap.get(requestField.getOrigin());
            if (generator == null) {
                readResponse.fail(requestField, ResponseCode.NO_VALUE);
            } else {
                readResponse.addValue(requestField, readValue(generator));
            }
        }
        readRequest.getRequestFields().stream()
                .map(thingField -> fieldGeneratorMap.get(thingField.getOrigin()))
                .filter(Objects::nonNull)
                .forEach(Generator::clearValue);
        return readResponse;
    }

    private Generator buildGenerator(ThingField requestField) {
        DataType dataType = requestField.getDataType();
        Boolean unsigned = requestField.getUnsigned();
        Integer arrayLength = requestField.getArrayLength();
        // TODO 此处仅处理数组与数据类型, 不考虑正负数,  若提供驱动需考虑
        Generator generator;
        if (arrayLength != null && arrayLength > 0) {
            // 数组情况
            generator = new Generator(arrayLength, dataType);
        } else {
            generator = new Generator(1, dataType);
        }
        return generator;
    }

    // TODO 数据类型转换, 数组, 正负数, 数据类型。
    private Object readValue(Generator generator) {
        return generator.getValue();
    }

    @Override
    public CompletableFuture<ReadResponse> asyncRead(ReadRequest readRequest) {
        return CompletableFuture.supplyAsync(() -> read(readRequest), executorService);
    }

    @Override
    public WriteResponse write(WriteRequest writeRequest) {
        DefaultWriteResponse writeResponse = new DefaultWriteResponse();
        writeRequest.forEach((field,value) -> {
            log.error(ConnFailCategory.REQUEST_ERROR, "Failed to write {} to {} , Reason : {} ", ThingField.info(field), value, "reason");
            writeResponse.addFailField(field, ResponseCode.UNSUPPORTED);
        });
        return writeResponse;
    }
}

Generator.java

生成数据的工具类

package com.hvisions.iot.drivers.simulator;

import com.hvisions.iot.utils.base.data.DataType;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;

import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Random;

/**
 * <p>Title: GenerateData</p>
 * <p>Description: 生成数据</p>
 * <p>Company: www.h-visions.com</p>
 * <p>create date: 2021/11/10</p>
 *
 * @author : xhjing
 * @version :1.0.0
 */

public class Generator {

    private final int length;

    private final Random random = new Random();

    private final String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    private final char[] chars = str.toCharArray();

    private final DecimalFormat format = new DecimalFormat("#.000");

    private final DataType dataType;

    private Object value;

    public Generator(Integer length, DataType dataType) {
        this.length = length;
        this.dataType = dataType;
    }

    public Object getValue() {
        if (value == null) {
            value = generateData();
        }
        return value;
    }

    public void clearValue() {
        value = null;
    }


    private Object generateData(){
        if (dataType == null) {
            return generateBooleanArray()[0];
        }
        // 数组
        if (length > 1) {
            switch (dataType) {
                case BYTE: return generateIntArray(127);
                case SHORT: return generateIntArray(32767);
                case INT: return generateIntArray();
                case LONG: return generateLongArray();
                case FLOAT: return generateFloatArray();
                case DOUBLE: return generateDoubleArray();
                case CHAR: return generateCharArray();
                case BOOLEAN: return generateBooleanArray();
                case DATE: return generateDateArray();
                case TIME: return generateTimeArray();
                case TIMESTAMP: return generateDateTimeArray();
                case WCHAR:
                case STRING:
                default: return generateStringArray();
            }
        } else {
            switch (dataType) {
                case BYTE: return generateIntArray(127)[0];
                case SHORT: return generateIntArray(32767)[0];
                case INT: return generateIntArray()[0];
                case LONG: return generateLongArray()[0];
                case FLOAT: return generateFloatArray()[0];
                case DOUBLE: return generateDoubleArray()[0];
                case CHAR: return generateCharArray()[0];
                case BOOLEAN: return generateBooleanArray()[0];
                case DATE: return LocalDate.now();
                case TIME: return LocalTime.now();
                case TIMESTAMP: return LocalDateTime.now();
                case WCHAR:
                case STRING:
                default: return generateStringArray()[0];
            }
        }
    }

    private int[] generateIntArray(int max) {
        int[] arr = new int[length];
        for(int i = 0; i < arr.length; i++){
            arr[i] = RandomUtils.nextInt(0,max);
        }
        return arr;
    }

    private int[] generateIntArray() {
        int[] arr = new int[length];
        for(int i = 0; i < arr.length; i++){
            arr[i] = RandomUtils.nextInt();
        }
        return arr;
    }

    private long[] generateLongArray() {
        long[] arr = new long[length];
        for(int i = 0; i < arr.length; i++){
            arr[i] = RandomUtils.nextLong();
        }
        return arr;
    }

    private float[] generateFloatArray() {
        float[] arr = new float[length];
        for(int i = 0; i < arr.length; i++){
            arr[i] = Float.parseFloat(format.format(RandomUtils.nextFloat(1,100)));
        }
        return arr;
    }


    private double[] generateDoubleArray() {
        double[] arr = new double[length];
        for(int i = 0; i < arr.length; i++){
            arr[i] = Double.parseDouble(format.format(RandomUtils.nextDouble(100,1000)));
        }
        return arr;
    }

    private char[] generateCharArray() {
        char[] arr = new char[length];
        for(int i = 0; i < arr.length; i++){
            arr[i] = chars[RandomUtils.nextInt(0,chars.length)];
        }
        return arr;
    }

    private boolean[] generateBooleanArray() {
        boolean[] arr = new boolean[length];
        for(int i = 0; i < arr.length; i++){
            arr[i] = random.nextBoolean();
        }
        return arr;
    }

    private String[] generateStringArray() {
        String[] arr = new String[length];
        for(int i = 0; i < arr.length; i++){
            arr[i] = RandomStringUtils.random(4,str);
        }
        return arr;
    }

    private LocalDate[] generateDateArray() {
        LocalDate[] arr = new LocalDate[length];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = LocalDate.ofEpochDay(RandomUtils.nextInt(0, 999999));
        }
        return arr;
    }

    private LocalTime[] generateTimeArray() {
        LocalTime[] arr = new LocalTime[length];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = LocalTime.ofSecondOfDay(RandomUtils.nextInt(0,86399));
        }
        return arr;
    }

    private LocalDateTime[] generateDateTimeArray() {
        LocalDateTime[] arr = new LocalDateTime[length];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = LocalDateTime.of(generateDateArray()[0], generateTimeArray()[0]);
        }
        return arr;
    }


}

除此之外还有其他接口,可根据需要自己实现。

SimulatorSetting.class

package com.hvisions.iot.extension;


import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

/**
 * <p>Title: SimpleExSetting</p>
 * <p>Description: </p>
 * <p>Company: www.h-visions.com</p>
 * <p>create date: 2021/11/1</p>
 *
 * @author : xhjing
 * @version :1.0.0
 */
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SimulatorSetting {
    // ip地址, 使用该字段接受前端传输 地址名
    @FieldConfig(label = "连接地址", required = true, order = 10)
    private String address;
	// 端口port, 使用该字段接受前端传输 端口
    @FieldConfig(label = "端口", required = true, configDataType = ConfigDataType.NUMBER, order = 20)
    private Integer port;
	// 连接超时, 使用该字段接受前端传输 超时
    @FieldConfig(label = "连接超时", description = "连接超时 单位:毫秒", initialValue = "5000", configDataType = ConfigDataType.NUMBER, order = 30)
    private Integer connectTimeout = 5000;
    // 其余的专有配置需要配合前端进行书写,使用高级属性传输。

}

4.结语

将代码书写完成,使用gradle打包工具,将此项目打包成为jar包,然后进行HiperMATRIX EdgeStation 的驱动管理界面中进行上传驱动,等待几分钟HiperMATRIX EdgeStation会重新启动并加载该jar包,用户可自行进行测试连接,若发生错误则自行从连接日志中进行查看。

image-20221021113052870

2024-10-14
0