当前HiperMATRIX EdgeStation集成一些常用的驱动以供设备连接,包括有mqtt, Siemens,opcua,allen-bradley,modbus等协议,详情查看用户手册中创建连接,然而在工业领域中存在有大量的硬件,硬件设备厂商较多,所支持的驱动连接不尽相同。为此,HiperMATRIX EdgeStation提供一种自定义驱动方式集成,用户可自我完成驱动的编写,以插件形式集成至HiperMATRIX EdgeStation,以此达到扩展目的。
So Let's Begin!
本文提供一个自定义驱动集成的案例进行实现, 用户可查看代码根据需要进行编写。
示例代码:example
1.开发环境
本文使用Java开发环境:
- 操作系统:Windows10
- JDK版本:JDK8
- 集成开发环境:IntelliJ IDEA社区版
- 使用Gradle进行项目整合
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包,用户可自行进行测试连接,若发生错误则自行从连接日志中进行查看。