commit 34ea26c4b633413096532369165c96dea82c44e2 Author: 小鱼干 <1810377322@163.com> Date: Fri Jun 6 18:04:03 2025 +0800 Initial commit diff --git a/.gitee/ISSUE_TEMPLATE.zh-CN.md b/.gitee/ISSUE_TEMPLATE.zh-CN.md new file mode 100644 index 0000000..f09d98d --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE.zh-CN.md @@ -0,0 +1,13 @@ +### 该问题是怎么引起的? + + + +### 重现步骤 + + + +### 报错信息 + + + + diff --git a/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md new file mode 100644 index 0000000..0ed1c31 --- /dev/null +++ b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md @@ -0,0 +1,53 @@ +### 一、内容说明(相关的Issue) + + + +### 二、建议测试周期和提测地址 + 建议测试完成时间:xxxx.xx.xx + 投产上线时间:xxxx.xx.xx + 提测地址:CI环境/压测环境 + 测试账号: + +### 三、变更内容 + * 3.1 关联PR列表 + + * 3.2 数据库和部署说明 + 1. 常规更新 + 2. 重启unicorn + 3. 重启sidekiq + 4. 迁移任务:是否有迁移任务,没有写 "无" + 5. rake脚本:`bundle exec xxx RAILS_ENV = production`;没有写 "无" + + * 3.4 其他技术优化内容(做了什么,变更了什么) + - 重构了 xxxx 代码 + - xxxx 算法优化 + + + * 3.5 废弃通知(什么字段、方法弃用?) + + + + * 3.6 后向不兼容变更(是否有无法向后兼容的变更?) + + + +### 四、研发自测点(自测哪些?冒烟用例全部自测?) + 自测测试结论: + + +### 五、测试关注点(需要提醒QA重点关注的、可能会忽略的地方) + 检查点: + +| 需求名称 | 是否影响xx公共模块 | 是否需要xx功能 | 需求升级是否依赖其他子产品 | +|------|------------|----------|---------------| +| xxx | 否 | 需要 | 不需要 | +| | | | | + + 接口测试: + + 性能测试: + + 并发测试: + + 其他: + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e2ac05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Compiled class file +*.class + +# Log file +log +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +.idea +target +*.iml +data/elasticsearch +.init +*.db +.flattened-pom.xml + +.DS_Store +dependency-reduced-pom.xml diff --git a/DLT645-plugin/pom.xml b/DLT645-plugin/pom.xml new file mode 100644 index 0000000..7489642 --- /dev/null +++ b/DLT645-plugin/pom.xml @@ -0,0 +1,87 @@ + + + + iot-iita-plugins + cc.iotkit.plugins + 2.0.19 + + 4.0.0 + + DLT645-plugin + + + + + io.vertx + vertx-core + ${vertx.version} + + + + io.netty + netty-transport + + + + junit + junit + 4.13.2 + test + + + + + + + dev + + true + + + dev + + + + + prod + + prod + + + + + + + + com.gitee.starblues + spring-brick-maven-packager + ${spring-brick.version} + + ${plugin.build.mode} + + DLT645-plugin + cc.iotkit.plugins.dlt645.Application + ${project.version} + iita + DLT645示例插件 + application.yml + + + jar + + + + + + repackage + + + + + + + + + \ No newline at end of file diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/Application.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/Application.java new file mode 100644 index 0000000..1a07d65 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/Application.java @@ -0,0 +1,21 @@ +package cc.iotkit.plugins.dlt645; + +import com.gitee.starblues.bootstrap.SpringPluginBootstrap; +import com.gitee.starblues.bootstrap.annotation.OneselfConfig; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @Author:tfd + * @Date:2023/12/14 16:25 + */ +@SpringBootApplication(scanBasePackages = "cc.iotkit.plugins.dlt645") +@OneselfConfig(mainConfigFileName = {"application.yml"}) +@EnableConfigurationProperties +@EnableScheduling +public class Application extends SpringPluginBootstrap { + public static void main(String[] args) { + new Application().run(Application.class, args); + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Analysis.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Analysis.java new file mode 100644 index 0000000..5e10fd7 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Analysis.java @@ -0,0 +1,323 @@ +package cc.iotkit.plugins.dlt645.analysis; + +import cc.iotkit.plugins.dlt645.constants.DLT645Constant; +import cc.iotkit.plugins.dlt645.load.DLT645v1997CsvLoader; +import cc.iotkit.plugins.dlt645.load.DLT645v2007CsvLoader; +import cc.iotkit.plugins.dlt645.utils.ByteRef; +import cc.iotkit.plugins.dlt645.utils.BytesRef; +import cc.iotkit.plugins.dlt645.utils.ContainerUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @Author:tfd + * @Date:2023/12/13 17:51 + * DL/T 645-1997 通讯规约通信规约 + * 帧起始符 地址域 帧起始符 控制码 数据长度域 数据域 校验码 结束符 + * 1 6 1 1 1 N 1 1 + */ +public class DLT645Analysis { + /** + * 静态实例 + */ + private static final DLT645Analysis DLT645Analysis = new DLT645Analysis(); + + public static DLT645Analysis inst() { + return DLT645Analysis; + } + /** + * 设备地址:4字节的byte[] + */ + public static final String ADR = "ADR"; + /** + * 功能码:1字节的byte + */ + public static final String FUN = "FUN"; + /** + * 数据报:不定长的byte[] + */ + public static final String DAT = "DAT"; + + /** + * 索引表 + */ + private Map name2entity; + public static Map din2entity; + + + /** + * 验证码 + * + * @param arrCmd + * @param iOffSet + * @return + */ + private static int GetVfy(byte[] arrCmd, int iOffSet) { + int iSize = arrCmd.length - 2 - iOffSet; + if (iSize < 0) { + return 0; + } + + int bySum = 0x00; + + int index = iOffSet; + for (int i = 0; i < iSize; i++) { + bySum += arrCmd[index++]; + bySum &= 0xff; + } + + return bySum & 0xff; + } + + /** + * 打包 + * + * @param arrAddr 6字节的地址码 + * @param byCmd 命令字 + * @param arrData 数据段 + * @return 是否成功 + */ + public static byte[] packCmd(byte[] arrAddr, byte byCmd, byte[] arrData) { + // 检查:数据块的大小 + int iDataSize = arrData.length; + if (iDataSize > 255) { + return new byte[0]; + } + if (arrAddr.length != 6) { + return new byte[0]; + } + + // 初始化数组大小 + byte[] arrCmd = new byte[iDataSize + 13]; + + + int index = 0; + + + // 前导字符(在发送帧信息之前,先发送1个或多个字节FEH,以唤醒接收方) + arrCmd[index++] = (byte) 0xFE; + + // 帧起始符 + arrCmd[index++] = (byte) 0x68; + + // 地址码 + System.arraycopy(arrAddr, 0, arrCmd, index, arrAddr.length); + index += arrAddr.length; + + // 帧起始符 + arrCmd[index++] = (byte) 0x68; + + // 控制码 + arrCmd[index++] = byCmd; + + // 帧长度 + arrCmd[index++] = (byte) iDataSize; + + // 数据域 + System.arraycopy(arrData, 0, arrCmd, index, iDataSize); + // 每个字节加上0x33 + for (int i = 0; i < arrData.length; i++) { + arrCmd[index + i] = (byte) ((arrCmd[index + i] & 0xff) + 0x33); + } + index += iDataSize; + + + // 校验码 + arrCmd[index++] = (byte) GetVfy(arrCmd, 1); + + // 结束符 + arrCmd[index++] = 0x16; + + return arrCmd; + } + + /** + * 默认打包 + * + * @param byCmd + * @param arrData + * @return + */ + public static byte[] packCmd(byte byCmd, byte[] arrData) { + byte[] arrAddr = new byte[6]; + + arrAddr[0] = 0x01; + arrAddr[1] = 0x00; + arrAddr[2] = 0x00; + arrAddr[3] = 0x00; + arrAddr[4] = 0x00; + arrAddr[5] = 0x00; + + return packCmd(arrAddr, byCmd, arrData); + } + + /** + * 解包 + * + * @param arrCmd 报文,前面有不确定的唤醒字符 + * @param arrAddrRef 地址码 + * @param byCmd 命令字 + * @param arrDataRef 数据 + * @return 是否成功 + */ + private static boolean unPackCmd2Map(byte[] arrCmd, BytesRef arrAddrRef, ByteRef byCmd, BytesRef arrDataRef) { + int iSize = arrCmd.length; + + // 查找偏移量:DLT645电表前面会被塞入不定长的乱码数据,被用来激活电表,直到0x68字符出现 + int iOffSet = 0; + int index = 0; + for (iOffSet = 0; iOffSet < iSize; iOffSet++) { + if ((arrCmd[index++] & 0xff) == 0x68) { + break; + } + } + if (iOffSet == iSize) { + return false; + } + + // 检查:数据包大小 + if (iSize < 12 + iOffSet) { + return false; + } + +//============================================================================== +// 中国电力总局的DL/T 645-1997 多功能电能表通信规约 +// 引导码 起始符 地址码 起始符 功能码 帧长度 数据域 校验和 结束符 +// N 1 6 1 1 1 N 1 1 +//============================================================================== + + // 检查:起始符1 + if (arrCmd[iOffSet + 0] != 0x68) { + return false; + } + // 检查:起始符2 + if (arrCmd[iOffSet + 7] != 0x68) { + return false; + } + // 检查:结束符 + if (arrCmd[iSize - 1] != 0x16) { + return false; + } + + // 地址码 + byte[] arrAddr = new byte[6]; + System.arraycopy(arrCmd, iOffSet + 1, arrAddr, 0, 6); + arrAddrRef.setValue(arrAddr); + + + // 功能码 + byCmd.setValue(arrCmd[iOffSet + 8]); + + + // 检查:帧长度 + int iDataSize = arrCmd[iOffSet + 9]; + if ((iDataSize + 12 + iOffSet) != iSize) { + return false; + } + + // 数据域 + byte[] arrData = new byte[iDataSize]; + System.arraycopy(arrCmd, iOffSet + 10, arrData, 0, iDataSize); + // 每个字节先减去0x33 + for (int i = 0; i < arrData.length; i++) { + arrData[i] = (byte) ((arrData[i] & 0xff) - 0x33); + } + arrDataRef.setValue(arrData); + + + // 检查:校验码 + byte byVfyOK = (byte) (GetVfy(arrCmd, iOffSet) & 0xff); + return byVfyOK == arrCmd[iSize - 2]; + } + + public static boolean unPackCmd2Map(byte[] arrCmd, ByteRef byCmd, BytesRef arrData) { + BytesRef arrAddr = new BytesRef(); + return unPackCmd2Map(arrCmd, arrAddr, byCmd, arrData); + } + + /** + * 只有数据标识的DI0和DI1的请求命令 + * + * @param DI0 数据标识 + * @param DI1 数据标识 + * @return + */ + public static byte[] packCmdGetData(int DI0, int DI1) { + byte[] arrData = new byte[2]; + arrData[0] = (byte) DI0; + arrData[1] = (byte) DI1; + + return packCmd((byte) 0x01, arrData); + } + + public static boolean unPackCmdGetData(byte[] arrCmd, BytesRef arrData) { + ByteRef byCmd = new ByteRef(); + if (!unPackCmd2Map(arrCmd, byCmd, arrData)) { + return false; + } + + return byCmd.getValue() == 0x81; + } + + /** + * 包装成另一种格式 + * + * @param arrCmd + * @return + */ + public static Map unPackCmd2Map(byte[] arrCmd) { + ByteRef byFun = new ByteRef(); + BytesRef byAddr = new BytesRef(); + BytesRef arrData = new BytesRef(); + if (!unPackCmd2Map(arrCmd, byAddr, byFun, arrData)) { + return Collections.emptyMap(); + } + + Map value = new HashMap<>(); + value.put(ADR, byAddr.getValue()); + value.put(FUN, byFun.getValue()); + value.put(DAT, arrData.getValue()); + return value; + } + + /** + * 获得对象信息 + * + * @return 对象副本 + */ + public synchronized Map getTemplateByName() { + if (this.name2entity == null) { + DLT645v1997CsvLoader loader = new DLT645v1997CsvLoader(); + List entityList = loader.loadCsvFile(); + Map nameMap = ContainerUtils.buildMapByKey(entityList, DLT645V1997Data::getName); + this.name2entity = new ConcurrentHashMap<>(); + this.name2entity.putAll(nameMap); + } + + return this.name2entity; + } + + public synchronized Map getTemplateByDIn(String ver) { + if (this.din2entity == null) { + if(DLT645Constant.PRO_VER_1997.equals(ver)){ + DLT645v1997CsvLoader loader = new DLT645v1997CsvLoader(); + List entityList = loader.loadCsvFile(); + Map keyMap = ContainerUtils.buildMapByKey(entityList, DLT645V1997Data::getKey); + this.din2entity = new ConcurrentHashMap<>(); + this.din2entity.putAll(keyMap); + }else{ + DLT645v2007CsvLoader loader = new DLT645v2007CsvLoader(); + List entityList = loader.loadCsvFile(); + Map keyMap = ContainerUtils.buildMapByKey(entityList, DLT645V2007Data::getKey); + this.din2entity = new ConcurrentHashMap<>(); + this.din2entity.putAll(keyMap); + } + } + + return this.din2entity; + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Converter.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Converter.java new file mode 100644 index 0000000..dd98e76 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Converter.java @@ -0,0 +1,44 @@ +package cc.iotkit.plugins.dlt645.analysis; + +import cc.iotkit.plugins.dlt645.constants.DLT645Constant; +import cc.iotkit.plugins.dlt645.utils.ByteUtils; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + + +/** + * @Author:tfd + * @Date:2023/12/13 17:54 + */ +@Slf4j +@Data +public class DLT645Converter { + + public static String packData(String deviceAddress,String funCode,String dataIdentifier){ + // 对设备地址进行编码 + byte[] tmp = ByteUtils.hexStringToByteArray(deviceAddress); + byte[] adrr = new byte[6]; + ByteUtils.byteInvertedOrder(tmp,adrr); + + // 根据对象名获取对象格式信息,这个格式信息,记录在CSV文件中 + DLT645Data dataEntity = DLT645Analysis.inst().getTemplateByDIn(DLT645Constant.PRO_VER_2007).get(dataIdentifier); + if (dataEntity == null) { + log.info("CSV模板文件中未定义对象:" + dataIdentifier + " ,你需要在模板中添加该对象信息"); + } + byte byFun = Byte.decode(String.valueOf(DLT645FunCode.getCode(funCode,DLT645Constant.PRO_VER_2007))); + + // 使用DLT645协议框架编码 + byte[] pack = DLT645Analysis.packCmd(adrr,byFun,dataEntity.getDIn()); + + // 将报文按要求的16进制格式的String对象返回 + return ByteUtils.byteArrayToHexString(pack,false); + } + @Data + public static class ReportData{ + private String type; + private String identifier; + private Long occur; + private Long time; + private Object data; + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Data.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Data.java new file mode 100644 index 0000000..44b5063 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Data.java @@ -0,0 +1,92 @@ +package cc.iotkit.plugins.dlt645.analysis; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * @Author:tfd + * @Date:2023/12/13 17:55 + */ +@Slf4j +@Data +public abstract class DLT645Data { + /** + * 名称 + */ + private String name; + /** + * 格式 + */ + private DLT645DataFormat format = new DLT645DataFormat(); + /** + * 长度 + */ + private int length; + /** + * 单位 + */ + private String unit; + /** + * 可读 + */ + private boolean read; + /** + * 可写 + */ + private boolean write; + /** + * 数值 + */ + private Object value = 0.0; + + /** + * 数值 + */ + private Object value2nd; + + public abstract String getKey(); + + public abstract byte[] getDIn(); + + public abstract void setDIn(byte[] value); + + public abstract int getDInLen(); + + public String toString() { + if (this.value2nd == null) { + return this.name + ":" + this.value + this.unit; + } + + return this.name + ":" + this.value + this.unit + " " + this.value2nd; + } + + public void decodeValue(byte[] data, Map dinMap) { + + // DI值 + this.setDIn(data); + + // 获取字典信息 + DLT645Data dict = dinMap.get(this.getKey()); + if (dict == null) { + log.info("DIn info err,please configure:" + this.getKey()); + } + + this.format = dict.format; + this.name = dict.name; + this.read = dict.read; + this.write = dict.write; + this.length = dict.length; + this.unit = dict.unit; + + + // 基本值 + this.value = this.format.decodeValue(data, this.format.getFormat(), this.getDInLen(), this.format.getLength()); + + // 组合值 + if (this.format.getFormat2nd() != null && !this.format.getFormat2nd().isEmpty()) { + this.value2nd = this.format.decodeValue(data, this.format.getFormat2nd(), this.getDInLen() + this.format.getLength(), this.format.getLength2nd()); + } + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645DataFormat.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645DataFormat.java new file mode 100644 index 0000000..3715947 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645DataFormat.java @@ -0,0 +1,361 @@ +package cc.iotkit.plugins.dlt645.analysis; + +import cc.iotkit.plugins.dlt645.utils.ByteUtils; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @Author:tfd + * @Date:2023/12/13 17:56 + */ +@Slf4j +@Data +public class DLT645DataFormat { + // 数值格式 + public static final String FORMAT_X = "X"; + public static final String FORMAT_N = "N"; + + // 时间格式 + public static final String FORMAT_YYMMDDWW = "YYMMDDWW"; + public static final String FORMAT_hhmmss = "hhmmss"; + public static final String FORMAT_YYMMDDhhmm = "YYMMDDhhmm"; + public static final String FORMAT_MMDDhhmm = "MMDDhhmm"; + public static final String FORMAT_DDhh = "DDhh"; + public static final String FORMAT_hhmm = "hhmm"; + public static final String FORMAT_mmmm = "mmmm"; + + // 编码格式 + public static final String FORMAT_NN___NN = "NN...NN"; + public static final String FORMAT_XX___XX = "XX...XX"; + + // 状态格式 + public static final String FORMAT_STATUS_WEEK = "周休日状态字"; + public static final String FORMAT_STATUS_METER = "电表运行状态字"; + public static final String FORMAT_STATUS_NETWORK = "电网状态字"; + + + /** + * 格式类型 + */ + private String format = ""; + /** + * 组合格式:第二个格式 + */ + private String format2nd = ""; + /** + * 长度 + */ + private int length = 0; + /** + * 组合格式:第二个长度 + */ + private int length2nd = 0; + /** + * 缩小比例 + */ + private double ratio = 1.0; + + public Object decodeValue(byte[] data, String format, int start, int length) throws RuntimeException { + // 前面4个字节是DI0~DI3 + if (data.length < length + start) { + log.info("DATA LENGTH ERROR"); + } + + // 各种XX.XX格式 + if (format.equals(FORMAT_X)) { + return this.getValue(data, start, length, this.ratio); + } + // 各种NN.NN格式 + if (format.equals(FORMAT_N)) { + return this.getValue(data, start, length, this.ratio); + } + if (format.equals(FORMAT_NN___NN)) { + return this.getString(data, start, length); + } + + // 时间格式 + if (format.equals(FORMAT_hhmm) || format.equals(FORMAT_DDhh) || format.equals(FORMAT_YYMMDDWW) || format.equals(FORMAT_hhmmss) || format.equals(FORMAT_YYMMDDhhmm) || format.equals(FORMAT_MMDDhhmm)) { + return this.getDataTime(data, format, start, length); + } + + + if (format.equals(FORMAT_XX___XX)) { + this.format = FORMAT_XX___XX; + this.ratio = 1.0; + return true; + } + + + return false; + } + + /** + * 解码格式:固定长度格式和可变长度格式 + * 固定长度格式:根据XX.XX它格式本身长度进行判定 + * + * @param format 格式名称 + * @param length 可变格式的长度 + * @return 是否成功 + */ + public boolean decodeFormat(String format, int length) { + // 统计字符种类的数量 + Map charCount = charCount(format); + + // 组合格式:XX.XXXX|YYMMDDhhmm + if (charCount.containsKey('|') && charCount.get('|').equals(1)) { + String format1 = format.substring(0, format.indexOf("|")); + String format2 = format.substring(format.indexOf("|") + 1); + this.decodeFormat(format2, -1); + this.format2nd = this.format; + this.length2nd = this.length; + this.decodeFormat(format1, -1); + return true; + } + + // 各种XX.XX格式 + if (charCount.containsKey('X') && charCount.containsKey('.') && charCount.get('.').equals(1)) { + this.format = FORMAT_X; + int point = format.length() - format.indexOf(".") - 1; + for (int i = 0; i < point; i++) { + this.ratio *= 10.0; + } + this.length = (format.length() - 1) / 2; + return true; + } + // XXXX格式 + if (charCount.containsKey('X') && charCount.size() == 1) { + this.format = FORMAT_X; + this.ratio = 1.0; + this.length = length; + return true; + } + // 各种NN.NN格式 + if (charCount.containsKey('N') && charCount.containsKey('.') && charCount.get('.').equals(1)) { + this.format = FORMAT_N; + int point = format.length() - format.indexOf(".") - 1; + for (int i = 0; i < point; i++) { + this.ratio *= 10.0; + } + this.length = (format.length() - 1) / 2; + return true; + } + // NNN格式 + if (charCount.containsKey('N') && charCount.size() == 1) { + this.format = FORMAT_N; + this.ratio = 1.0; + this.length = length; + return true; + } + + + // 固定长度 + if (this.isFixedLength(format)) { + this.format = format; + this.ratio = 1.0; + this.length = format.length() / 2; + return true; + } + + // 可变长度 + if (this.isVariableLength(format)) { + this.format = format; + this.ratio = 1.0; + this.length = length; + return true; + } + + return false; + } + + /** + * 是否为固定长度:它的长度是直接通过格式就能确定 + * + * @param format + * @return + */ + private boolean isFixedLength(String format) { + if (format.equalsIgnoreCase(FORMAT_hhmm)) { + return true; + } + if (format.equalsIgnoreCase(FORMAT_DDhh)) { + return true; + } + if (format.equalsIgnoreCase(FORMAT_MMDDhhmm)) { + return true; + } + if (format.equalsIgnoreCase(FORMAT_YYMMDDhhmm)) { + return true; + } + if (format.equalsIgnoreCase(FORMAT_hhmmss)) { + return true; + } + if (format.equalsIgnoreCase(FORMAT_mmmm)) { + return true; + } + + return format.equalsIgnoreCase(FORMAT_YYMMDDWW); + } + + /** + * 是否为可变长度:它的长度是通过用户在CSV文件中告知 + * + * @param format + * @return + */ + private boolean isVariableLength(String format) { + if (format.equalsIgnoreCase(FORMAT_NN___NN)) { + return true; + } + if (format.equalsIgnoreCase(FORMAT_XX___XX)) { + return true; + } + if (format.equalsIgnoreCase(FORMAT_STATUS_METER)) { + return true; + } + if (format.equalsIgnoreCase(FORMAT_STATUS_NETWORK)) { + return true; + } + + return format.equalsIgnoreCase(FORMAT_STATUS_WEEK); + } + + /** + * 统计字符数量,方面后面判定格式 + * + * @param format DLT645规约中定义的XXX.XXX之类的各种数据格式文本 + * @return 各字符数量 + */ + private Map charCount(String format) { + + Map charSet = new HashMap<>(); + for (int i = 0; i < format.length(); i++) { + Character ch = format.charAt(i); + Integer count = charSet.get(ch); + if (count == null) { + count = 0; + } + + count++; + charSet.put(ch, count); + } + + return charSet; + } + + /** + * 4字节长度的double型数值 + * + * @param data data数组 + * @param start 数据在数组中的起始位置 + * @param ratio 倍率,比如缩小100倍数,那么填0.01 + * @return 返回值 + */ + private Object getValue(byte[] data, int start, int length, double ratio) { + long sum = 0; + double rd = 1.0; + for (int i = 0; i < length; i++) { + long l = data[start + i] & 0x0f; + long h = (data[start + i] & 0xf0) >> 4; + + l = (long) (l * rd); + sum += l; + rd = rd * 10.0; + + + h = (long) (h * rd); + sum += h; + rd = rd * 10.0; + + } + + if (ratio < 1.1 && ratio > 0.0) { + // 如果ratio==1 + return sum; + } else { + return sum / ratio; + } + } + + /** + * 日期格式的解码 + * + * @param data data数组 + * @param format 日期格式 + * @param start 数据在数组中的起始位置 + * @param length 格式长度 + * @return 返回值 + */ + private String getDataTime(byte[] data, String format, int start, int length) { + // 拆解成个位数列表 + List list = new ArrayList<>(); + for (int i = 0; i < length; i++) { + int l = data[start + i] & 0x0f; + int h = (data[start + i] & 0xf0) >> 4; + + list.add(Integer.toString(l)); + list.add(Integer.toString(h)); + } + + // 格式1 + if (format.equals(FORMAT_YYMMDDhhmm)) { + String result = "20" + list.get(9) + list.get(8) + "年" + list.get(7) + list.get(6) + "月" + list.get(5) + list.get(4) + "日"; + result += " " + list.get(3) + list.get(2) + "点" + list.get(1) + list.get(0) + "分"; + return result; + } + + if (format.equals(FORMAT_YYMMDDWW)) { + String result = "20" + list.get(7) + list.get(6) + "年" + list.get(5) + list.get(4) + "月" + list.get(3) + list.get(2) + "日"; + result += " 星期:" + list.get(1) + list.get(0); + return result; + } + if (format.equals(FORMAT_hhmmss)) { + return list.get(5) + list.get(4) + "点" + list.get(3) + list.get(2) + "分" + list.get(1) + list.get(0) + "秒"; + } + if (format.equals(FORMAT_mmmm)) { + return list.get(3) + list.get(2) + list.get(1) + list.get(0) + "分"; + } + + + if (format.equals(FORMAT_MMDDhhmm)) { + String result = list.get(7) + list.get(6) + "月" + list.get(5) + list.get(4) + "日 "; + result += list.get(3) + list.get(2) + "点" + list.get(1) + list.get(0) + "分"; + return result; + } + if (format.equals(FORMAT_DDhh)) { + return list.get(3) + list.get(2) + "号 " + list.get(1) + list.get(0) + "点"; + } + if (format.equals(FORMAT_hhmm)) { + return list.get(3) + list.get(2) + "点 " + list.get(1) + list.get(0) + "分"; + } + + + return ""; + } + + private byte encodeBCD(byte a) { + return (byte) ((a / 10) * 16 + (a % 10)); + } + + private byte decodeBCD(byte a) { + return (byte) ((a / 16) * 10 + (a % 16)); + } + + private Object getString(byte[] data, int start, int length) { + byte[] tmp = new byte[length]; + + for (int i = 0; i < length; i++) { + tmp[i] = data[start + i]; + } + for (int i = 0; i < length / 2; i++) { + byte by = tmp[i]; + tmp[i] = tmp[length - i - 1]; + tmp[length - i - 1] = by; + } + return ByteUtils.byteArrayToHexString(tmp, true).replace(" ", ""); + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645FunCode.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645FunCode.java new file mode 100644 index 0000000..825998c --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645FunCode.java @@ -0,0 +1,302 @@ +package cc.iotkit.plugins.dlt645.analysis; + +import cc.iotkit.plugins.dlt645.constants.DLT645Constant; +import lombok.Data; + +/** + * @Author:tfd + * @Date:2023/12/13 17:58 + */ +@Data +public class DLT645FunCode { + + /* 2007 */ + private static final String func_v07_00000 = "保留"; + private static final String func_v07_01000 = "广播校时"; + private static final String func_v07_10001 = "读数据"; + private static final String func_v07_10010 = "读后续数据"; + private static final String func_v07_10011 = "读通信地址"; + private static final String func_v07_10100 = "写数据"; + private static final String func_v07_10101 = "写通信地址"; + private static final String func_v07_10110 = "冻结"; + private static final String func_v07_10111 = "更改通信速率"; + private static final String func_v07_11000 = "修改密码"; + private static final String func_v07_11001 = "最大需量清零"; + private static final String func_v07_11010 = "电表清零"; + private static final String func_v07_11011 = "事件清零"; + + /* 1997 */ + public static final String func_v97_00000 = "保留"; + public static final String func_v97_00001 = "读数据"; + public static final String func_v97_00010 = "读后续数据"; + public static final String func_v97_00011 = "重读数据"; + public static final String func_v97_00100 = "写数据"; + public static final String func_v97_01000 = "广播校时"; + public static final String func_v97_01010 = "写设备地址"; + public static final String func_v97_01100 = "更改通信速率"; + public static final String func_v97_01111 = "修改密码"; + public static final String func_v97_10000 = "最大需量清零"; + /** + * 方向:主站发出=false,从站应答=true + */ + private boolean direct = false; + /** + * 从站是否异常应答 + */ + private boolean error = false; + /** + * 功能代码 + */ + private byte code = 0; + /** + * 是否最后的尾部 + */ + private boolean next = false; + + public static DLT645FunCode decodeEntity(byte func) { + DLT645FunCode dlt645FunCode = new DLT645FunCode(); + dlt645FunCode.decode(func); + return dlt645FunCode; + } + + public static int getCodev2007(String text) { + if (func_v07_00000.equals(text)) { + return 0b00000; + } + if (func_v07_01000.equals(text)) { + return 0b01000; + } + if (func_v07_10001.equals(text)) { + return 0b10001; + } + if (func_v07_10010.equals(text)) { + return 0b10010; + } + if (func_v07_10011.equals(text)) { + return 0b10011; + } + if (func_v07_10100.equals(text)) { + return 0b10100; + } + if (func_v07_10101.equals(text)) { + return 0b10101; + } + if (func_v07_10110.equals(text)) { + return 0b10110; + } + if (func_v07_10111.equals(text)) { + return 0b10111; + } + if (func_v07_11000.equals(text)) { + return 0b11000; + } + if (func_v07_11001.equals(text)) { + return 0b11001; + } + if (func_v07_11010.equals(text)) { + return 0b11010; + } + if (func_v07_11011.equals(text)) { + return 0b11011; + } + return 0b00000; + } + + public static int getCodev1997(String text) { + if (func_v97_00000.equals(text)) {// + return 0b00000; + } + if (func_v97_00001.equals(text)) { + return 0b00001; + } + if (func_v97_00010.equals(text)) { + return 0b00010; + } + if (func_v97_00011.equals(text)) { + return 0b00011; + } + if (func_v97_00100.equals(text)) { + return 0b00100; + } + if (func_v97_01000.equals(text)) { + return 0b01000; + } + if (func_v97_01010.equals(text)) { + return 0b01010; + } + if (func_v97_01100.equals(text)) { + return 0b01100; + } + if (func_v97_01111.equals(text)) { + return 0b01111; + } + if (func_v97_10000.equals(text)) { + return 0b10000; + } + return 0b00000; + } + + /** + * 编码 + * + * @return 功能码 + */ + public byte encode() { + int func = 0; + if (this.direct) { + func |= 0x80; + } + if (this.error) { + func |= 0x40; + } + if (this.next) { + func |= 0x20; + } + func |= this.code & 0x1F; + + return (byte) func; + } + + /** + * 生成功能码 + * + * @param dlt645FunCode + * @return + */ + public byte encodeFunCode(DLT645FunCode dlt645FunCode) { + return dlt645FunCode.encode(); + } + + /** + * 解码 + * + * @param func + */ + public void decode(byte func) { + this.direct = (func & 0x80) > 0; + this.error = (func & 0x40) > 0; + this.next = (func & 0x20) > 0; + this.code = (byte) (func & 0x1F); + } + + public static int getCode(String text,String ver){ + if(DLT645Constant.PRO_VER_1997.equals(ver)){ + return getCodev1997(text); + }else{ + return getCodev2007(text); + } + } + + public String getCodeTextV1997() { + if (this.code == 0b00000) { + return func_v97_00000; + } + if (this.code == 0b01000) { + return func_v97_01000; + } + if (this.code == 0b00001) { + return func_v97_00001; + } + if (this.code == 0b00010) { + return func_v97_00010; + } + if (this.code == 0b00100) { + return func_v97_00100; + } + if (this.code == 0b01010) { + return func_v97_01010; + } + if (this.code == 0b01100) { + return func_v97_01100; + } + if (this.code == 0b01111) { + return func_v97_01111; + } + if (this.code == 0b10000) { + return func_v97_10000; + } + + return ""; + } + + public String getCodeTextV2007() { + if (this.code == 0b00000) { + return func_v07_00000; + } + if (this.code == 0b01000) { + return func_v07_01000; + } + if (this.code == 0b10001) { + return func_v07_10001; + } + if (this.code == 0b10010) { + return func_v07_10010; + } + if (this.code == 0b10011) { + return func_v07_10011; + } + if (this.code == 0b10100) { + return func_v07_10100; + } + if (this.code == 0b10101) { + return func_v07_10101; + } + if (this.code == 0b10110) { + return func_v07_10110; + } + if (this.code == 0b10111) { + return func_v07_10111; + } + if (this.code == 0b11000) { + return func_v07_11000; + } + if (this.code == 0b11001) { + return func_v07_11001; + } + if (this.code == 0b11010) { + return func_v07_11010; + } + if (this.code == 0b11011) { + return func_v07_11011; + } + + return ""; + } + + /** + * 获取文本描述 + * + * @return 文本描述 + */ + public String getMessage(String ver) { + String message = ""; + if (this.direct) { + message += "从站发出:"; + } else { + message += "主站发出:"; + } + + if (ver.equalsIgnoreCase(DLT645Constant.PRO_VER_1997)) { + message += this.getCodeTextV1997(); + } + if (ver.equalsIgnoreCase(DLT645Constant.PRO_VER_2007)) { + message += this.getCodeTextV2007(); + } + message += this.getCodeTextV1997(); + + if (this.error) { + message += ":异常"; + } else { + message += ":正常"; + } + + + if (this.next) { + message += ":还有后续帧"; + } else { + message += ":这是末尾帧"; + } + + return message; + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645V1997Data.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645V1997Data.java new file mode 100644 index 0000000..931496c --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645V1997Data.java @@ -0,0 +1,63 @@ +package cc.iotkit.plugins.dlt645.analysis; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * @Author:tfd + * @Date:2023/12/13 17:58 + */ +@Slf4j +@Setter +@Getter +public class DLT645V1997Data extends DLT645Data { + /** + * DI1/DI0 + */ + private byte di0l = 0; + private byte di0h = 0; + private byte di1l = 0; + private byte di1h = 0; + + @Override + public String getKey() { + String key = ""; + key += Integer.toString(this.di1h, 16); + key += Integer.toString(this.di1l, 16); + key += Integer.toString(this.di0h, 16); + key += Integer.toString(this.di0l, 16); + return key.toUpperCase(); + } + + @Override + public byte[] getDIn() { + byte[] value = new byte[2]; + value[0] = (byte) (this.di0l + (this.di0h << 4)); + value[1] = (byte) (this.di1l + (this.di1h << 4)); + return value; + } + + @Override + public void setDIn(byte[] value) { + if (value.length < 2) { + log.info("DATA LENGTH ERROR"); + } + + // DI值 + this.di1h = (byte) ((value[1] & 0xf0) >> 4); + this.di1l = (byte) (value[1] & 0x0f); + this.di0h = (byte) ((value[0] & 0xf0) >> 4); + this.di0l = (byte) (value[0] & 0x0f); + } + + /** + * 1997版的DIn2字节 + * + * @return + */ + @Override + public int getDInLen() { + return 2; + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645V2007Data.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645V2007Data.java new file mode 100644 index 0000000..3ad35eb --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645V2007Data.java @@ -0,0 +1,66 @@ +package cc.iotkit.plugins.dlt645.analysis; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** + * @Author:tfd + * @Date:2023/12/13 17:58 + */ +@Slf4j +@Setter +@Getter +public class DLT645V2007Data extends DLT645Data { + /** + * DI1/DI0 + */ + private byte di0 = 0; + private byte di1 = 0; + private byte di2 = 0; + private byte di3 = 0; + + + public String getKey() { + String key = ""; + key += StringUtils.leftPad(Integer.toHexString(this.di3 & 0xFF),2,"0"); + key += StringUtils.leftPad(Integer.toHexString(this.di2 & 0xFF),2,"0"); + key += StringUtils.leftPad(Integer.toHexString(this.di1 & 0xFF),2,"0"); + key += StringUtils.leftPad(Integer.toHexString(this.di0 & 0xFF),2,"0") + ""; + return key.toUpperCase(); + } + + @Override + public byte[] getDIn() { + byte[] value = new byte[4]; + value[0] = this.di0; + value[1] = this.di1; + value[2] = this.di2; + value[3] = this.di3; + + return value; + } + + @Override + public void setDIn(byte[] value) { + if (value.length < 4) { + log.info("DATA LENGTH ERROR"); + } + + this.di0 = value[0]; + this.di1 = value[1]; + this.di2 = value[2]; + this.di3 = value[3]; + } + + /** + * 2007版的DIn 4字节 + * + * @return + */ + @Override + public int getDInLen() { + return 4; + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/conf/BeanConfig.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/conf/BeanConfig.java new file mode 100644 index 0000000..9ff3013 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/conf/BeanConfig.java @@ -0,0 +1,39 @@ +package cc.iotkit.plugins.dlt645.conf; + +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.IPluginScript; +import cc.iotkit.plugin.core.LocalPluginConfig; +import cc.iotkit.plugin.core.LocalPluginScript; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugins.dlt645.service.FakeThingService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @Author:tfd + * @Date:2023/12/13 16:54 + */ +@Slf4j +@Component +public class BeanConfig { + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IThingService getThingService() { + return new FakeThingService(); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginScript getPluginScript() { + log.info("init LocalPluginScript"); + return new LocalPluginScript("script.js"); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginConfig getPluginConfig() { + return new LocalPluginConfig(); + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/conf/TcpClientConfig.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/conf/TcpClientConfig.java new file mode 100644 index 0000000..413a4c5 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/conf/TcpClientConfig.java @@ -0,0 +1,21 @@ +package cc.iotkit.plugins.dlt645.conf; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author:tfd + * @Date:2023/12/13 17:01 + */ +@Data +@Component +@ConfigurationProperties(prefix = "tcp") +public class TcpClientConfig { + + private String host; + + private int port; + + private int interval; +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/constants/DLT645Constant.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/constants/DLT645Constant.java new file mode 100644 index 0000000..f623531 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/constants/DLT645Constant.java @@ -0,0 +1,10 @@ +package cc.iotkit.plugins.dlt645.constants; + +/** + * @Author:tfd + * @Date:2023/12/22 10:54 + */ +public class DLT645Constant { + public static final String PRO_VER_1997 = "v1997"; + public static final String PRO_VER_2007 = "v2007"; +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/load/DLT645v1997CsvLoader.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/load/DLT645v1997CsvLoader.java new file mode 100644 index 0000000..83e9106 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/load/DLT645v1997CsvLoader.java @@ -0,0 +1,93 @@ +package cc.iotkit.plugins.dlt645.load; + +import cc.iotkit.plugins.dlt645.analysis.DLT645Data; +import cc.iotkit.plugins.dlt645.analysis.DLT645DataFormat; +import cc.iotkit.plugins.dlt645.analysis.DLT645V1997Data; +import cn.hutool.core.text.csv.CsvReader; +import cn.hutool.core.text.csv.CsvUtil; +import cn.hutool.core.util.CharsetUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.io.InputStreamReader; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * @Author:tfd + * @Date:2023/12/13 17:59 + */ +@Slf4j +public class DLT645v1997CsvLoader { + /** + * 从CSV文件中装载映射表 + * + */ + public List loadCsvFile() { + CsvReader csvReader = CsvUtil.getReader(); + InputStreamReader dataReader=new InputStreamReader(this.getClass().getClassLoader().getResourceAsStream("DLT645-1997.csv"),CharsetUtil.CHARSET_GBK); + List rows = csvReader.read(dataReader, JDecoderValueParam.class); + List list = new ArrayList<>(); + for (JDecoderValueParam jDecoderValueParam : rows) { + try { + DLT645V1997Data entity = new DLT645V1997Data(); + entity.setName(jDecoderValueParam.getName()); + entity.setDi1h((byte) Integer.parseInt(jDecoderValueParam.di1h, 16)); + entity.setDi1l((byte) Integer.parseInt(jDecoderValueParam.di1l, 16)); + entity.setDi0h((byte) Integer.parseInt(jDecoderValueParam.di0h, 16)); + entity.setDi0l((byte) Integer.parseInt(jDecoderValueParam.di0l, 16)); + entity.setLength(jDecoderValueParam.length); + entity.setUnit(jDecoderValueParam.unit); + entity.setRead(Boolean.parseBoolean(jDecoderValueParam.read)); + entity.setWrite(Boolean.parseBoolean(jDecoderValueParam.write)); + + DLT645DataFormat format = new DLT645DataFormat(); + if (format.decodeFormat(jDecoderValueParam.format, jDecoderValueParam.length)) { + entity.setFormat(format); + } else { + log.info("DLT645 CSV记录的格式错误:" + jDecoderValueParam.getName() + ":" + jDecoderValueParam.getFormat()); + continue; + } + list.add(entity); + } catch (Exception e) { + e.printStackTrace(); + } + } + return list; + } + + + @Data + public static class JDecoderValueParam implements Serializable { + private String di1h; + private String di1l; + private String di0h; + private String di0l; + /** + * 编码格式 + */ + private String format; + /** + * 长度 + */ + private Integer length; + /** + * 单位 + */ + private String unit; + + /** + * 是否可读 + */ + private String read; + /** + * 是否可写 + */ + private String write; + /** + * 名称 + */ + private String name; + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/load/DLT645v2007CsvLoader.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/load/DLT645v2007CsvLoader.java new file mode 100644 index 0000000..8d2e1fc --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/load/DLT645v2007CsvLoader.java @@ -0,0 +1,95 @@ +package cc.iotkit.plugins.dlt645.load; + +import cc.iotkit.plugins.dlt645.analysis.DLT645Data; +import cc.iotkit.plugins.dlt645.analysis.DLT645DataFormat; +import cc.iotkit.plugins.dlt645.analysis.DLT645V2007Data; +import cn.hutool.core.text.csv.CsvReader; +import cn.hutool.core.text.csv.CsvUtil; +import cn.hutool.core.util.CharsetUtil; +import io.vertx.core.AbstractVerticle; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.io.InputStreamReader; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * @Author:tfd + * @Date:2023/12/13 17:59 + */ +@Slf4j +public class DLT645v2007CsvLoader extends AbstractVerticle { + + /** + * 从CSV文件中装载映射表 + * + */ + public List loadCsvFile() { + CsvReader csvReader = CsvUtil.getReader(); + InputStreamReader dataReader=new InputStreamReader(this.getClass().getClassLoader().getResourceAsStream("DLT645-2007.csv"),CharsetUtil.CHARSET_GBK); + List rows = csvReader.read(dataReader, JDecoderValueParam.class); + List list = new ArrayList<>(); + for (JDecoderValueParam jDecoderValueParam : rows) { + try { + DLT645V2007Data entity = new DLT645V2007Data(); + entity.setName(jDecoderValueParam.getName()); + entity.setDi0((byte) Integer.parseInt(jDecoderValueParam.di0, 16)); + entity.setDi1((byte) Integer.parseInt(jDecoderValueParam.di1, 16)); + entity.setDi2((byte) Integer.parseInt(jDecoderValueParam.di2, 16)); + entity.setDi3((byte) Integer.parseInt(jDecoderValueParam.di3, 16)); + entity.setLength(jDecoderValueParam.length); + entity.setUnit(jDecoderValueParam.unit); + entity.setRead(Boolean.parseBoolean(jDecoderValueParam.read)); + entity.setWrite(Boolean.parseBoolean(jDecoderValueParam.write)); + + DLT645DataFormat format = new DLT645DataFormat(); + if (format.decodeFormat(jDecoderValueParam.format, jDecoderValueParam.length)) { + entity.setFormat(format); + } else { + log.info("DLT645 CSV记录的格式错误:" + jDecoderValueParam.getName() + ":" + jDecoderValueParam.getFormat()); + continue; + } + list.add(entity); + } catch (Exception e) { + e.printStackTrace(); + } + } + return list; + } + + + @Data + public static class JDecoderValueParam implements Serializable { + private String di0; + private String di1; + private String di2; + private String di3; + /** + * 编码格式 + */ + private String format; + /** + * 长度 + */ + private Integer length; + /** + * 单位 + */ + private String unit; + + /** + * 是否可读 + */ + private String read; + /** + * 是否可写 + */ + private String write; + /** + * 名称 + */ + private String name; + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/DLT645Device.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/DLT645Device.java new file mode 100644 index 0000000..8999ffe --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/DLT645Device.java @@ -0,0 +1,54 @@ +package cc.iotkit.plugins.dlt645.service; + +import cc.iotkit.plugin.core.thing.IDevice; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.down.DeviceConfig; +import cc.iotkit.plugin.core.thing.actions.down.PropertyGet; +import cc.iotkit.plugin.core.thing.actions.down.PropertySet; +import cc.iotkit.plugin.core.thing.actions.down.ServiceInvoke; +import cc.iotkit.plugins.dlt645.analysis.DLT645Converter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Arrays; + +/** + * @Author:tfd + * @Date:2023/12/14 16:22 + */ +@Service +public class DLT645Device implements IDevice { + + @Autowired + private TcpClientVerticle dlt645Verticle; + + @Override + public ActionResult config(DeviceConfig action) { + return ActionResult.builder().code(0).reason("").build(); + } + + public static void main(String[] args) { + String topic = "/JN10202310300068/s/event/property/post"; + String s = Arrays.asList(topic.split("/")).get(1); + topic = "/sys/*/"+s+"/c/event/property/post"; + System.out.println(topic); + } + @Override + public ActionResult propertyGet(PropertyGet action) { + for (String key:action.getKeys()){ + String msg=DLT645Converter.packData(action.getDeviceName(),"读数据",key.replaceFirst("p","")); + dlt645Verticle.sendMsg(msg); + } + return ActionResult.builder().code(0).reason("success").build(); + } + + @Override + public ActionResult propertySet(PropertySet action) { + return null; + } + + @Override + public ActionResult serviceInvoke(ServiceInvoke action) { + return null; + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/Dlt645Plugin.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/Dlt645Plugin.java new file mode 100644 index 0000000..17aa3cc --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/Dlt645Plugin.java @@ -0,0 +1,88 @@ +package cc.iotkit.plugins.dlt645.service; + +import cc.iotkit.common.utils.JsonUtils; +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugins.dlt645.conf.TcpClientConfig; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.bootstrap.realize.PluginCloseListener; +import com.gitee.starblues.core.PluginCloseType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * @Author:tfd + * @Date:2023/12/13 16:58 + */ +@Slf4j +@Service +public class Dlt645Plugin implements PluginCloseListener { + + @Autowired + private PluginInfo pluginInfo; + + @Autowired + private TcpClientVerticle tcpClientVerticle; + + @Autowired + private TcpClientConfig tcpConfig; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginConfig pluginConfig; + + private Vertx vertx; + private String deployedId; + + @PostConstruct + public void init() { + vertx = Vertx.vertx(); + try { + //获取插件最新配置替换当前配置 + Map config = pluginConfig.getConfig(pluginInfo.getPluginId()); + BeanUtil.copyProperties(config, tcpConfig, CopyOptions.create().ignoreNullValue()); + tcpClientVerticle.setConfig(tcpConfig); + + Future future = vertx.deployVerticle(tcpClientVerticle); + future.onSuccess((s -> { + deployedId = s; + log.info("dlt645 client plugin started success,config:"+ JsonUtils.toJsonString(tcpConfig)); + })); + future.onFailure(Throwable::printStackTrace); + } catch (Throwable e) { + log.error("init dlt645 client plugin error", e); + } + } + + @SneakyThrows + @Override + public void close(GenericApplicationContext applicationContext, PluginInfo pluginInfo, PluginCloseType closeType) { + log.info("plugin close,type:{},pluginId:{}", closeType, pluginInfo.getPluginId()); + if (deployedId != null) { + CountDownLatch wait = new CountDownLatch(1); + Future future = vertx.undeploy(deployedId); + future.onSuccess(unused -> { + log.info("dlt645 client plugin stopped success"); + wait.countDown(); + }); + future.onFailure(h -> { + log.info("dlt645 client plugin stopped failed"); + h.printStackTrace(); + wait.countDown(); + }); + wait.await(5, TimeUnit.SECONDS); + } + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/FakeThingService.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/FakeThingService.java new file mode 100644 index 0000000..303f859 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/FakeThingService.java @@ -0,0 +1,46 @@ +package cc.iotkit.plugins.dlt645.service; + +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author:tfd + * @Date:2023/12/13 16:56 + */ +@Slf4j +public class FakeThingService implements IThingService { + + @Override + public ActionResult post(String pluginId, IDeviceAction action) { + log.info("post action:{}", action); + return ActionResult.builder().code(0).build(); + } + + @Override + public ThingProduct getProduct(String pk) { + return ThingProduct.builder() + .productKey("PjmkANSTDt85bZPj") + .productSecret("aaaaaaaa") + .build(); + } + + @Override + public ThingDevice getDevice(String dn) { + return ThingDevice.builder() + .productKey("PjmkANSTDt85bZPj") + .deviceName(dn) + .build(); + } + + @Override + public Map getProperty(String dn) { + return new HashMap<>(0); + } +} \ No newline at end of file diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/TcpClientVerticle.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/TcpClientVerticle.java new file mode 100644 index 0000000..b3132bc --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/TcpClientVerticle.java @@ -0,0 +1,184 @@ +package cc.iotkit.plugins.dlt645.service; + +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.DeviceState; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.actions.up.DeviceStateChange; +import cc.iotkit.plugin.core.thing.actions.up.PropertyReport; +import cc.iotkit.plugins.dlt645.analysis.DLT645Analysis; +import cc.iotkit.plugins.dlt645.analysis.DLT645Converter; +import cc.iotkit.plugins.dlt645.analysis.DLT645FunCode; +import cc.iotkit.plugins.dlt645.analysis.DLT645V2007Data; +import cc.iotkit.plugins.dlt645.conf.TcpClientConfig; +import cc.iotkit.plugins.dlt645.constants.DLT645Constant; +import cc.iotkit.plugins.dlt645.utils.ByteUtils; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.IdUtil; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @Author:tfd + * @Date:2023/12/13 17:00 + */ +@Slf4j +@Service +public class TcpClientVerticle extends AbstractVerticle { + @Getter + @Setter + private TcpClientConfig config; + + private NetClient netClient; + + private NetSocket socket; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + + @Autowired + private PluginInfo pluginInfo; + + private int connectState = 0; + + private long timerID; + + @Override + public void start() { + log.info("init start"); + } + + @Scheduled(initialDelay = 2, fixedRate = 5, timeUnit = TimeUnit.SECONDS) + public void initClient() { + if (connectState > 0) { + return; + } + connectState = 1; + + DLT645Analysis.inst().getTemplateByDIn(DLT645Constant.PRO_VER_2007); + NetClientOptions options = new NetClientOptions(); + options.setReconnectAttempts(Integer.MAX_VALUE); + options.setReconnectInterval(20000L); + netClient = vertx.createNetClient(options); + netClient.connect(config.getPort(), config.getHost()) + .onComplete(result -> { + if (result.succeeded()) { + connectState = 2; + log.info("connect dlt645 server success"); + socket = result.result(); + stateChange(DeviceState.ONLINE); + socket.handler(data -> { + String hexStr = ByteUtils.byteArrayToHexString(data.getBytes(), false); + log.info("Received message:{}", hexStr); + Map ret = DLT645Analysis.unPackCmd2Map(ByteUtils.hexStringToByteArray(hexStr)); + //获取功能码 + Object func = ret.get(DLT645Analysis.FUN); + DLT645FunCode funCode = DLT645FunCode.decodeEntity((byte) func); + if (funCode.isError()) { + log.info("message erroe:{}", hexStr); + return; + } + //获取设备地址 + byte[] adrrTmp = (byte[]) ret.get(DLT645Analysis.ADR); + byte[] addr = new byte[6]; + ByteUtils.byteInvertedOrder(adrrTmp, addr); + //获取数据 + byte[] dat = (byte[]) ret.get(DLT645Analysis.DAT); + String strAddr=ByteUtils.byteArrayToHexString(addr,false); + DLT645V2007Data dataEntity = new DLT645V2007Data(); + dataEntity.decodeValue(dat, DLT645Analysis.din2entity); + Map params = new HashMap<>(); + params.put("p" + dataEntity.getKey(), dataEntity.getValue());//数据标识 + thingService.post(pluginInfo.getPluginId(), + PropertyReport.builder().deviceName(strAddr).productKey("PwMfpXmp4ZWkGahn") + .params(params) + .build() + ); + }).closeHandler(res -> { + connectState = 0; + vertx.cancelTimer(timerID); + log.info("dlt645 tcp connection closed!"); + stateChange(DeviceState.OFFLINE); + } + ).exceptionHandler(res -> { + connectState = 0; + vertx.cancelTimer(timerID); + log.info("dlt645 tcp connection exce!"); + stateChange(DeviceState.OFFLINE); + }); + timerID = vertx.setPeriodic(config.getInterval(), t -> { + readDataTask(); + }); + } else { + connectState = 0; + log.info("connect dlt645 tcp error", result.cause()); + } + }) + .onFailure(e -> { + log.error("connect failed", e); + connectState = 0; + }) + ; + } + + private void readDataTask() { + log.info("readData:" + socket); + if (socket != null) { + String msg = DLT645Converter.packData("000023092701", "读数据", "00000000"); + sendMsg(msg); + } + } + + @Override + public void stop() throws Exception { + if (netClient != null) { + netClient.close(); + } + vertx.cancelTimer(timerID); + connectState = 0; + super.stop(); + } + + private void stateChange(DeviceState state) { + thingService.post(pluginInfo.getPluginId(), + applyPkDn(DeviceStateChange.builder() + .id(IdUtil.simpleUUID()) + .state(state) + .time(System.currentTimeMillis()) + .build())); + } + + private IDeviceAction applyPkDn(IDeviceAction action) { + action.setProductKey("BRD3x4fkKxkaxXFt"); + action.setDeviceName("WG123456"); + return action; + } + + public void sendMsg(String msg) { + log.info("send msg data:{}", msg); + Buffer data = Buffer.buffer(HexUtil.decodeHex(msg)); + socket.write(data, r -> { + if (r.succeeded()) { + log.info("msg send success:{}", msg); + } else { + log.error("msg send failed", r.cause()); + } + }); + } + +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ByteRef.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ByteRef.java new file mode 100644 index 0000000..7d24120 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ByteRef.java @@ -0,0 +1,15 @@ +package cc.iotkit.plugins.dlt645.utils; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +/** + * @Author:tfd + * @Date:2023/12/13 17:49 + */ +@Getter(value = AccessLevel.PUBLIC) +@Setter(value = AccessLevel.PUBLIC) +public class ByteRef { + private byte value = 0; +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ByteUtils.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ByteUtils.java new file mode 100644 index 0000000..fd58b51 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ByteUtils.java @@ -0,0 +1,78 @@ +package cc.iotkit.plugins.dlt645.utils; + +/** + * @Author:tfd + * @Date:2023/12/13 17:48 + */ +public class ByteUtils { + /** + * 根据十六进制生成byte + * + * @param hex 16进制的字符串 比如"FF" + * @return byte数字比如-1 + */ + public static byte hex2byte(String hex) { + return Integer.valueOf(hex, 16).byteValue(); + } + + /** + * 16进制的字符串表示转成字节数组 + * + * @param hexString 16进制格式的字符串 + * @return 转换后的字节数组 + **/ + public static byte[] hexStringToByteArray(String hexString) { + String string = hexString.replaceAll(" ", ""); + final byte[] byteArray = new byte[string.length() / 2]; + int pos = 0; + for (int i = 0; i < byteArray.length; i++) { + // 因为是16进制,最多只会占用4位,转换成字节需要两个16进制的字符,高位在先 + byte high = (byte) (Character.digit(string.charAt(pos), 16) & 0xff); + byte low = (byte) (Character.digit(string.charAt(pos + 1), 16) & 0xff); + byteArray[i] = (byte) (high << 4 | low); + pos += 2; + } + + return byteArray; + } + + /** + * 字节数组转成16进制表示格式的字符串 + * + * @param byteArray 要转换的字节数组 + * @return 16进制表示格式的字符串 + **/ + public static String byteArrayToHexString(byte[] byteArray) { + return byteArrayToHexString(byteArray, true); + } + + public static String byteArrayToHexString(byte[] byteArray, boolean blankz) { + final StringBuilder hexString = new StringBuilder(); + for (int i = 0; i < byteArray.length; i++) { + if ((byteArray[i] & 0xff) < 0x10) { + // 0~F前面不零 + hexString.append("0"); + } + + hexString.append(Integer.toHexString(0xFF & byteArray[i])); + + if (blankz) { + hexString.append(" "); + } + } + return hexString.toString(); + } + + /** + * 字节逆序 + * + **/ + public static void byteInvertedOrder(byte[] tmp,byte[] retData) { + System.arraycopy(tmp, 0, retData, 0, Math.min(tmp.length, retData.length)); + for (int i = 0; i < retData.length / 2; i++) { + byte by = retData[i]; + retData[i] = retData[5 - i]; + retData[5 - i] = by; + } + } +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/BytesRef.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/BytesRef.java new file mode 100644 index 0000000..a93589f --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/BytesRef.java @@ -0,0 +1,15 @@ +package cc.iotkit.plugins.dlt645.utils; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +/** + * @Author:tfd + * @Date:2023/12/13 17:49 + */ +@Getter(value = AccessLevel.PUBLIC) +@Setter(value = AccessLevel.PUBLIC) +public class BytesRef { + private byte[] value = new byte[] {}; +} diff --git a/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ContainerUtils.java b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ContainerUtils.java new file mode 100644 index 0000000..6579315 --- /dev/null +++ b/DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ContainerUtils.java @@ -0,0 +1,424 @@ +package cc.iotkit.plugins.dlt645.utils; + +import java.io.Serializable; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.function.Function; + +/** + * @Author:tfd + * @Date:2023/12/13 17:50 + * 集合操作的工具类 + */ +public class ContainerUtils { + public ContainerUtils() { + } + + /** + * 获取类函数的名称 + * 例如:getMethodName(Integer.toHexString),返回的就是toHexString + * + * @param function + * @param + * @param + * @return + * @throws NoSuchMethodException + * @throws SecurityException + * @throws IllegalAccessException + * @throws IllegalArgumentException + * @throws InvocationTargetException + */ + private static String getMethodName(SerializableFunction function) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + Method method = function.getClass().getDeclaredMethod("writeReplace"); + method.setAccessible(Boolean.TRUE); + SerializedLambda serializedLambda = (SerializedLambda) method.invoke(function); + String implMethodName = serializedLambda.getImplMethodName(); + + return implMethodName; + } + + /** + * 获取类的函数 + * + * @param clazz + * @param function + * @param + * @param + * @param + * @return + * @throws NoSuchMethodException + * @throws SecurityException + * @throws IllegalAccessException + * @throws IllegalArgumentException + * @throws InvocationTargetException + */ + private static Method getMethod(Class clazz, SerializableFunction function) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + String methodName = ContainerUtils.getMethodName(function); + return clazz.getMethod(methodName); + } + + /** + * 根据对象列表中的对象的getXxxx()函数,取出成员 + * + * @param objList + * @param clazz + * @param method method是clazz的成员函数 + * @param + * @param + * @return + */ + private static List buildListByGetField(List objList, Class clazz, Method method) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + List keyList = new ArrayList(); + for (T obj : objList) { + // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数 + Object keyObject = method.invoke(obj); + if (clazz.isInstance(keyObject)) { + K key = clazz.cast(keyObject); + keyList.add(key); + } + } + + return keyList; + } + + public static List buildKeyList(List objList, Method method) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + List keyList = new ArrayList(); + Class returnType = method.getReturnType(); + for (T obj : objList) { + // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数 + Object keyObject = method.invoke(obj); + if (returnType.isInstance(keyObject)) { + keyList.add((K) returnType.cast(keyObject)); + } + } + + return keyList; + } + + /** + * 根据对象的getXxxx(),取出类型列表中的数据 + * + * @param objList AClass对象列表 + * @param clazz TClass AClass::getXxxx()中,TClass这样的成员 + * @param function AClass::getXxxx 这样的函数 + * @param + * @param + * @param + * @param + * @return + */ + public static List buildListByGetField(List objList, SerializableFunction function, Class clazz) { + if (objList.isEmpty()) { + return new ArrayList(); + } + + try { + // 取得函数对应的方法 + Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function); + + // 使用方法返回对应的数组 + return ContainerUtils.buildListByGetField(objList, clazz, method); + } catch (NoSuchMethodException e) { + return new ArrayList(); + } catch (SecurityException e) { + return new ArrayList(); + } catch (IllegalAccessException e) { + return new ArrayList(); + } catch (IllegalArgumentException e) { + return new ArrayList(); + } catch (InvocationTargetException e) { + return new ArrayList(); + } + } + + /** + * 根据Key生成Map,该方法是是具体类的函数,(不具备多态能力,不是反射,速度很快) + * + * @param objList + * @param clazz + * @param method 使用obj.getClass().getMethod("getTnlKey", new Class[0])获取Method + * @return + */ + public static Map buildMapByKeyAndFinalMethod(List objList, Class clazz, Method method) { + try { + Map uid2deviceMap = new HashMap(); + for (T obj : objList) { + // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数 + Object keyObject = method.invoke(obj); + if (clazz.isInstance(keyObject)) { + K key = clazz.cast(keyObject); + uid2deviceMap.put(key, obj); + } + } + + return uid2deviceMap; + } catch (IllegalAccessException e) { + return Collections.emptyMap(); + } catch (InvocationTargetException e) { + return Collections.emptyMap(); + } + } + + + public static Map buildMapByKey(List objList, SerializableFunction function) { + if (objList.isEmpty()) { + return new HashMap<>(); + } + + try { + // 取得函数对应的方法 + Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function); + + // 使用方法返回对应的数组 + return ContainerUtils.buildMapByKey(objList, method); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IllegalArgumentException | + SecurityException e) { + return new HashMap(); + } + } + + public static Map buildMapByKey(List objList, Method method) { + try { + Map uid2deviceMap = new HashMap(); + for (T obj : objList) { + // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数 + Object keyObject = method.invoke(obj); + uid2deviceMap.put((K) keyObject, obj); + } + + return uid2deviceMap; + } catch (IllegalAccessException | InvocationTargetException e) { + return Collections.emptyMap(); + } + } + + /** + * 分类 + * + * @param objList + * @param clazz + * @param method + * @param + * @param + * @return + */ + public static Map> buildMapByTypeAndFinalMethod(List objList, Class clazz, Method method) { + try { + Map> uid2deviceMap = new HashMap<>(); + for (T obj : objList) { + // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数 + Object keyObject = method.invoke(obj); + if (clazz.isInstance(keyObject)) { + K key = clazz.cast(keyObject); + List list = uid2deviceMap.computeIfAbsent(key, k -> new ArrayList<>()); + list.add(obj); + } + } + return uid2deviceMap; + } catch (IllegalAccessException | InvocationTargetException e) { + return Collections.emptyMap(); + } + } + + /** + * 根据Key生成Map,该方法是是具体类的函数,(不具备多态能力,不是反射,速度很快) + * + * @param objList 类型AClass的列表容器 + * @param function 类型AClass中的某个成员getxxxx() + * @param clazz 类型AClass中的某个成员BClass getxxxx()中的BClass + * @param + * @param + * @param + * @param + * @return + */ + public static Map buildMapByKeyAndFinalMethod(List objList, SerializableFunction function, Class clazz) { + if (objList.isEmpty()) { + return new HashMap<>(); + } + + try { + // 取得函数对应的方法 + Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function); + + // 使用方法返回对应的数组 + return ContainerUtils.buildMapByKeyAndFinalMethod(objList, clazz, method); + } catch (NoSuchMethodException e) { + return new HashMap(); + } catch (SecurityException e) { + return new HashMap(); + } catch (IllegalAccessException e) { + return new HashMap(); + } catch (IllegalArgumentException e) { + return new HashMap(); + } catch (InvocationTargetException e) { + return new HashMap(); + } + } + + public static Map> buildMapByTypeAndFinalMethod(List objList, SerializableFunction function, Class clazz) { + if (objList.isEmpty()) { + return new HashMap<>(); + } + + try { + // 取得函数对应的方法 + Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function); + + // 使用方法返回对应的数组 + return ContainerUtils.buildMapByTypeAndFinalMethod(objList, clazz, method); + } catch (NoSuchMethodException | InvocationTargetException | IllegalArgumentException | IllegalAccessException | + SecurityException e) { + return new HashMap<>(); + } + } + + /** + * 根据map中的某个元素,将列表转换成以这个元素为key的map + * + * @param objList + * @param mapKey + * @param clazz + * @param + * @param + * @return + */ + public static Map> buildMapByMapAt(List> objList, String mapKey, Class clazz) { + if (objList.isEmpty()) { + return new HashMap<>(); + } + + Map> result = new HashMap<>(); + for (Map obj : objList) { + Object value = obj.get(mapKey); + if (!clazz.isInstance(value)) { + continue; + } + + result.put((K) value, obj); + } + + return result; + } + + /** + * 从列表中获取:某个字段等于某个值的对象 + * + * @param objList + * @param function + * @param key + * @param + * @param + * @param + * @param + * @return + */ + public static T getObjectByKey(List objList, SerializableFunction function, K key) { + try { + if (objList.isEmpty()) { + return null; + } + + Method method = ContainerUtils.getMethod(objList.get(0).getClass(), function); + + for (T obj : objList) { + // 先获取相应的method对象,getMethod第一个参数是方法名,第二个参数是该方法的参数类型, + // Method method = obj.getClass().getMethod(getKeyMathName, new Class[0]); + + // 接下来就该执行该方法了,第一个参数是具体调用该方法的对象, 第二个参数是执行该方法的具体参数 + Object keyObject = method.invoke(obj); + if (key.getClass().isInstance(keyObject)) { + if (keyObject.equals(key)) { + return obj; + } + } + } + + return null; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + return null; + } + } + + /** + * 交换Key-Value + * + * @param key2value key2value + * @return value2key + */ + public static Map exchange(Map key2value) { + Map result = new HashMap<>(); + + for (Map.Entry entry : key2value.entrySet()) { + result.put(entry.getValue(), entry.getKey()); + } + + return result; + } + + public static Map> exchanges(Map key2value) { + Map> result = new HashMap<>(); + + for (Map.Entry entry : key2value.entrySet()) { + List list = result.get(entry.getValue()); + if (list == null) { + list = new ArrayList<>(); + result.put(entry.getValue(), list); + } + + list.add(entry.getKey()); + } + + return result; + } + + /** + * 根据Key提取出相关的值列表 + * + * @param key2value key2value + * @param keyList key列表 + * @return 跟key相关的values + */ + public static List buildValueListByKey(Map key2value, Collection keyList) { + List resultList = new ArrayList(); + for (K key : keyList) { + V value = key2value.get(key); + if (value != null) { + resultList.add(value); + } + } + + return resultList; + } + + /** + * 从A类型列表转换成B类型列表:A/B是派生类关系 + * + * @param aClazzList + * @param bClazz + * @return + */ + public static List buildClassList(List aClazzList, Class bClazz) { + List bInstanceList = new ArrayList(); + + for (A aInstance : aClazzList) { + if (bClazz.isInstance(aInstance)) { + bInstanceList.add(bClazz.cast(aInstance)); + } + } + + return bInstanceList; + } + + /** + * 定义一个函数接口 + * + * @param + * @param + */ + @FunctionalInterface + public interface SerializableFunction extends Function, Serializable { + } +} diff --git a/DLT645-plugin/src/main/resources/DLT645-1997.csv b/DLT645-plugin/src/main/resources/DLT645-1997.csv new file mode 100644 index 0000000..bdc2685 --- /dev/null +++ b/DLT645-plugin/src/main/resources/DLT645-1997.csv @@ -0,0 +1,143 @@ +di1h,di1l,di0h,di0l,format,length,unit,read,write,name +9,0,1,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йܵ +9,0,2,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йܵ +9,1,1,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)޹ܵ +9,1,2,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)޹ܵ +9,1,3,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)һ޹ܵ +9,1,4,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)޹ܵ +9,1,5,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)޹ܵ +9,1,6,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)޹ܵ +9,4,1,0,XXXXXX.XX,4,kWh,TRUE,FALSE,()йܵ +9,4,2,0,XXXXXX.XX,4,kWh,TRUE,FALSE,()йܵ +9,5,1,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +9,5,2,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +9,5,3,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()һ޹ܵ +9,5,4,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +9,5,5,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +9,5,6,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +9,8,1,0,XXXXXX.XX,4,kWh,TRUE,FALSE,()йܵ +9,8,2,0,XXXXXX.XX,4,kWh,TRUE,FALSE,()йܵ +9,9,1,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +9,9,2,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +9,9,3,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()һ޹ܵ +9,9,4,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +9,9,5,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +9,9,6,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,()޹ܵ +A,0,1,0,XX.XXXX,3,kW,TRUE,FALSE,(ǰ)й +A,0,2,0,XX.XXXX,3,kW,TRUE,FALSE,(ǰ)й +A,1,1,0,XX.XXXX,3,kvar,TRUE,FALSE,(ǰ)޹ +A,1,2,0,XX.XXXX,3,kvar,TRUE,FALSE,(ǰ)޹ +A,1,3,0,XX.XXXX,3,kvar,TRUE,FALSE,(ǰ)һ޹ +A,1,4,0,XX.XXXX,3,kvar,TRUE,FALSE,(ǰ)޹ +A,1,5,0,XX.XXXX,3,kvar,TRUE,FALSE,(ǰ)޹ +A,1,6,0,XX.XXXX,3,kvar,TRUE,FALSE,(ǰ)޹ +A,4,1,0,XX.XXXX,3,kW,TRUE,FALSE,()й +A,4,2,0,XX.XXXX,3,kW,TRUE,FALSE,()й +A,5,1,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +A,5,2,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +A,5,3,0,XX.XXXX,3,kvar,TRUE,FALSE,()һ޹ +A,5,4,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +A,5,5,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +A,5,6,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +A,8,1,0,XX.XXXX,3,kvar,TRUE,FALSE,()й +A,8,2,0,XX.XXXX,3,kvar,TRUE,FALSE,()й +A,9,1,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +A,9,2,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +A,9,3,0,XX.XXXX,3,kvar,TRUE,FALSE,()һ޹ +A,9,4,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +A,9,5,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +A,9,6,0,XX.XXXX,3,kvar,TRUE,FALSE,()޹ +B,0,1,0,MMDDHHmm,4,,TRUE,FALSE,(ǰ)йʱ +B,0,2,0,MMDDHHmm,4,,TRUE,FALSE,(ǰ)йʱ +B,1,1,0,MMDDHHmm,4,,TRUE,FALSE,(ǰ)޹ʱ +B,1,2,0,MMDDHHmm,4,,TRUE,FALSE,(ǰ)޹ʱ +B,1,3,0,MMDDHHmm,4,,TRUE,FALSE,(ǰ)һ޹ʱ +B,1,4,0,MMDDHHmm,4,,TRUE,FALSE,(ǰ)޹ʱ +B,1,5,0,MMDDHHmm,4,,TRUE,FALSE,(ǰ)޹ʱ +B,1,6,0,MMDDHHmm,4,,TRUE,FALSE,(ǰ)޹ʱ +B,4,1,0,MMDDHHmm,4,,TRUE,FALSE,()йʱ +B,4,2,0,MMDDHHmm,4,,TRUE,FALSE,()йʱ +B,5,1,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,5,2,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,5,3,0,MMDDHHmm,4,,TRUE,FALSE,()һ޹ʱ +B,5,4,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,5,5,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,5,6,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,8,1,0,MMDDHHmm,4,,TRUE,FALSE,()йʱ +B,8,2,0,MMDDHHmm,4,,TRUE,FALSE,()йʱ +B,9,1,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,9,2,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,9,3,0,MMDDHHmm,4,,TRUE,FALSE,()һ޹ʱ +B,9,4,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,9,5,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,9,6,0,MMDDHHmm,4,,TRUE,FALSE,()޹ʱ +B,2,1,0,MMDDHHmm,4,,TRUE,FALSE,һαʱ +B,2,1,1,MMDDHHmm,4,,TRUE,FALSE,һʱ +B,2,1,2,NNNN,2,,TRUE,FALSE,̴ +B,2,1,3,NNNN,2,,TRUE,FALSE, +B,2,1,4,NNNNNN,3,min,TRUE,FALSE,عʱ +B,3,1,0,NNNN,2,,TRUE,FALSE,ܶ +B,3,1,1,NNNN,2,,TRUE,FALSE,A +B,3,1,2,NNNN,2,,TRUE,FALSE,B +B,3,1,3,NNNN,2,,TRUE,FALSE,C +B,3,2,0,NNNNNN,3,min,TRUE,FALSE,ʱۼֵ +B,3,2,1,NNNNNN,3,min,TRUE,FALSE,A ʱۼֵ +B,3,2,2,NNNNNN,3,min,TRUE,FALSE,B ʱۼֵ +B,3,2,3,NNNNNN,3,min,TRUE,FALSE,C ʱۼֵ +B,3,3,0,MMDDHHmm,4,,TRUE,FALSE,һζʼʱ +B,3,3,1,MMDDHHmm,4,,TRUE,FALSE,A ʼʱ +B,3,3,2,MMDDHHmm,4,,TRUE,FALSE,B ʼʱ +B,3,3,3,MMDDHHmm,4,,TRUE,FALSE,Cʼʱ +B,3,4,0,MMDDHHmm,4,,TRUE,FALSE,һζĽʱ +B,3,4,1,MMDDHHmm,4,,TRUE,FALSE,A һζĽʱ +B,3,4,2,MMDDHHmm,4,,TRUE,FALSE,B һζĽʱ +B,3,4,3,MMDDHHmm,4,,TRUE,FALSE,C һζĽʱ +B,6,1,1,XXX,2,V,TRUE,FALSE,Aѹ +B,6,1,2,XXX,2,V,TRUE,FALSE,Bѹ +B,6,1,3,XXX,2,V,TRUE,FALSE,Cѹ +B,6,2,1,XX.XX,2,A,TRUE,FALSE,A +B,6,2,2,XX.XX,2,A,TRUE,FALSE,B +B,6,2,3,XX.XX,2,A,TRUE,FALSE,C +B,6,3,0,XX.XXXX,3,kW,TRUE,FALSE,˲ʱй +B,6,3,1,XX.XXXX,3,kW,TRUE,FALSE,Aй +B,6,3,2,XX.XXXX,3,kW,TRUE,FALSE,Bй +B,6,3,3,XX.XXXX,3,kW,TRUE,FALSE,Cй +B,6,3,4,XX.XX,2,kW,TRUE,TRUE,йֵ +B,6,3,5,XX.XX,2,kW,TRUE,TRUE,йֵ +B,6,4,0,XX.XX,2,kvarh,TRUE,FALSE,˲ʱ޹ +B,6,4,1,XX.XX,2,kvarh,TRUE,FALSE,A޹ +B,6,4,2,XX.XX,2,kvarh,TRUE,FALSE,B޹ +B,6,4,3,XX.XX,2,kvarh,TRUE,FALSE,C޹ +B,6,5,0,XX.XX,2,kvarh,TRUE,FALSE,ܹ +B,6,5,1,XX.XX,3,kvarh,TRUE,FALSE,A ๦ +B,6,5,2,XX.XX,4,kvarh,TRUE,FALSE,B ๦ +B,6,5,3,XX.XX,5,kvarh,TRUE,FALSE,C ๦ +C,0,1,0,YYMMDDWW,4,,TRUE,TRUE,ڼܴ +C,0,1,1,hhmmss,3,,TRUE,TRUE,ʱ +C,0,2,0,״̬,1,,TRUE,TRUE,״̬ +C,0,2,1,״̬,1,,TRUE,TRUE,״̬ +C,0,2,2,״̬,1,,TRUE,TRUE,״̬ +C,0,3,0,NNNNNN,3,p/(kWh),TRUE,TRUE,(й) +C,0,3,1,NNNNNN,3,p/(kvarh),TRUE,TRUE,(޹) +C,0,3,2,NN...NN,6,,TRUE,TRUE, +C,0,3,3,NN...NN,6,,TRUE,TRUE,û +C,0,3,4,NN...NN,6,,TRUE,TRUE,豸 +C,1,1,1,XX,1,min,TRUE,TRUE, +C,1,1,2,XX,1,min,TRUE,TRUE,ʱ +C,1,1,3,XX,1,s,TRUE,TRUE,ѭʱ +C,1,1,4,XX,1,s,TRUE,TRUE,ͣʱ +C,1,1,5,NN,1,,TRUE,TRUE,ʾСλ +C,1,1,6,NN,1,,TRUE,TRUE,ʾ()Сλ +C,1,1,7,DDhh,2,,TRUE,TRUE,Զ +C,1,1,8,NN,1,,TRUE,TRUE,ɴ +C,1,1,9,NNNNNN.N,4,kWh,TRUE,TRUE,йʼ +C,1,1,A,NNNNNN.N,4,kvarh,TRUE,TRUE,޹ʼ +C,2,1,1,NNNN,2,ms,TRUE,TRUE, +C,2,1,2,NNNNNNNN,4,,FALSE,TRUE,Ȩ޼ +C,3,1,0,NN,1,,TRUE,TRUE,ʱ +C,3,1,1,NN,1,,TRUE,TRUE,ʱα +C,3,1,2,NN,1,,TRUE,TRUE,ʱ(ÿл)m10 +C,3,1,3,NN,1,,TRUE,TRUE, k14 +C,3,1,4,NN,1,,TRUE,TRUE, +C,5,1,0,MMDDhhmm,4,,TRUE,TRUE,ɼ¼ʼʱ +C,5,1,1,mmmm,2,min,TRUE,TRUE,ɼ¼ʱ diff --git a/DLT645-plugin/src/main/resources/DLT645-2007.csv b/DLT645-plugin/src/main/resources/DLT645-2007.csv new file mode 100644 index 0000000..d70ccf5 --- /dev/null +++ b/DLT645-plugin/src/main/resources/DLT645-2007.csv @@ -0,0 +1,425 @@ +di3,di2,di1,di0,format,length,unit,read,write,name +0,0,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йܵ +0,1,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йܵ +0,2,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йܵ +0,3,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)޹1ܵ +0,4,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)޹2ܵ +0,5,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)һ޹ܵ +0,6,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)ڶ޹ܵ +0,7,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)޹ܵ +0,8,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)޹ܵ +0,9,0,0,XXXXXX.XX,4,kVAh,TRUE,FALSE,(ǰ)ܵ +0,A,0,0,XXXXXX.XX,4,kVAh,TRUE,FALSE,(ǰ)ܵ +0,B,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,ǰйۼõ +0,80,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)ܵ +0,81,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йܵ +0,82,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йܵ +0,83,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йгܵ +0,84,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йгܵ +0,85,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)ͭйܵܲ +0,86,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)йܵܲ +0,15,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)Aй +0,16,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)A෴й +0,17,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)A޹1 +0,18,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)A޹2 +0,19,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)Aһ޹ +0,1A,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)Aڶ޹ +0,1B,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)A޹ +0,1C,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)A޹ +0,1D,0,0,XXXXXX.XX,4,kVAh,TRUE,FALSE,(ǰ)Aڵ +0,1E,0,0,XXXXXX.XX,4,kVAh,TRUE,FALSE,(ǰ)A෴ڵ +0,29,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)Bй +0,2A,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)B෴й +0,2B,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)B޹1 +0,2C,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)B޹2 +0,2D,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)Bһ޹ +0,2E,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)Bڶ޹ +0,2F,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)B޹ +0,30,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)B޹ +0,31,0,0,XXXXXX.XX,4,kVAh,TRUE,FALSE,(ǰ)Bڵ +0,32,0,0,XXXXXX.XX,4,kVAh,TRUE,FALSE,(ǰ)B෴ڵ +0,3D,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)Cй +0,3E,0,0,XXXXXX.XX,4,kWh,TRUE,FALSE,(ǰ)C෴й +0,3F,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)C޹1 +0,40,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)C޹2 +0,41,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)Cһ޹ +0,42,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)Cڶ޹ +0,43,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)C޹ +0,44,0,0,XXXXXX.XX,4,kvarh,TRUE,FALSE,(ǰ)C޹ +0,45,0,0,XXXXXX.XX,4,kVAh,TRUE,FALSE,(ǰ)Cڵ +0,46,0,0,XXXXXX.XX,4,kVAh,TRUE,FALSE,(ǰ)C෴ڵ +0,0,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)йܵ +0,1,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)йܵ +0,2,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)йܵ +0,3,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)޹1ܵ +0,4,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)޹2ܵ +0,5,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)һ޹ܵ +0,6,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)ڶ޹ܵ +0,7,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)޹ܵ +0,8,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)޹ܵ +0,9,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)ܵ +0,A,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)ܵ +0,80,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)ܵ +0,81,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)йܵ +0,82,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)йܵ +0,83,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)йгܵ +0,84,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)йгܵ +0,85,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)ͭйܵܲ +0,86,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)йܵܲ +0,15,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)Aй +0,16,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)A෴й +0,17,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)A޹1 +0,18,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)A޹2 +0,19,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)Aһ޹ +0,1A,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)Aڶ޹ +0,1B,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)A޹ +0,1C,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)A޹ +0,1D,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)Aڵ +0,1E,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)A෴ڵ +0,29,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)Bй +0,2A,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)B෴й +0,2B,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)B޹1 +0,2C,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)B޹2 +0,2D,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)Bһ޹ +0,2E,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)Bڶ޹ +0,2F,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)B޹ +0,30,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)B޹ +0,31,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)Bڵ +0,32,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)B෴ڵ +0,3D,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)Cй +0,3E,0,1,XXXXXX.XX,4,kWh,TRUE,FALSE,(1)C෴й +0,3F,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)C޹1 +0,40,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)C޹2 +0,41,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)Cһ޹ +0,42,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)Cڶ޹ +0,43,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)C޹ +0,44,0,1,XXXXXX.XX,4,kvarh,TRUE,FALSE,(1)C޹ +0,45,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)Cڵ +0,46,0,1,XXXXXX.XX,4,kVAh,TRUE,FALSE,(1)C෴ڵ +0,0,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)йܵ +0,1,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)йܵ +0,2,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)йܵ +0,3,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)޹1ܵ +0,4,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)޹2ܵ +0,5,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)һ޹ܵ +0,6,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)ڶ޹ܵ +0,7,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)޹ܵ +0,8,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)޹ܵ +0,9,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)ܵ +0,A,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)ܵ +0,80,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)ܵ +0,81,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)йܵ +0,82,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)йܵ +0,83,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)йгܵ +0,84,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)йгܵ +0,85,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)ͭйܵܲ +0,86,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)йܵܲ +0,15,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)Aй +0,16,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)A෴й +0,17,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)A޹1 +0,18,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)A޹2 +0,19,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)Aһ޹ +0,1A,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)Aڶ޹ +0,1B,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)A޹ +0,1C,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)A޹ +0,1D,0,C,XXXXXX.XX,4,kVAh,TRUE,FALSE,(12)Aڵ +0,1E,0,C,XXXXXX.XX,4,kVAh,TRUE,FALSE,(12)A෴ڵ +0,9,1,0,XXXXXX.XX,4,kWh,TRUE,FALSE,ǰʣ +0,9,1,1,XXXXXX.XX,4,kWh,TRUE,FALSE,ǰ͸֧ +0,9,2,0,XXXXXX.XX,4,Ԫ,TRUE,FALSE,ǰʣ +0,9,2,1,XXXXXX.XX,4,Ԫ,TRUE,FALSE,ǰ͸֧ +0,94,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)A +0,95,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)Aй +0,96,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)A෴й +0,97,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)Aйг +0,98,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)A෴йг +0,99,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)Aͭйܲ +0,9A,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)Aйܲ +0,29,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)Bй +0,2A,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)B෴й +0,2B,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)B޹1 +0,2C,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)B޹2 +0,2D,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)Bһ޹ +0,2E,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)Bڶ޹ +0,2F,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)B޹ +0,30,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)B޹ +0,31,0,C,XXXXXX.XX,4,kVAh,TRUE,FALSE,(12)Bڵ +0,32,0,C,XXXXXX.XX,4,kVAh,TRUE,FALSE,(12)B෴ڵ +0,3D,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)Cй +0,3E,0,C,XXXXXX.XX,4,kWh,TRUE,FALSE,(12)C෴й +0,3F,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)C޹1 +0,40,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)C޹2 +0,41,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)Cһ޹ +0,42,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)Cڶ޹ +0,43,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)C޹ +0,44,0,C,XXXXXX.XX,4,kvarh,TRUE,FALSE,(12)C޹ +0,45,0,C,XXXXXX.XX,4,kVAh,TRUE,FALSE,(12)Cڵ +0,46,0,C,XXXXXX.XX,4,kVAh,TRUE,FALSE,(12)C෴ڵ +1,1,0,0,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(ǰ)йʱ +1,2,0,0,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(ǰ)йʱ +1,3,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)޹1ʱ +1,4,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)޹2ʱ +1,5,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)һ޹ʱ +1,6,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)ڶ޹ʱ +1,7,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)޹ʱ +1,8,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)޹ʱ +1,9,0,0,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(ǰ)ʱ +1,A,0,0,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(ǰ)ʱ +1,15,0,0,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(ǰ)Aйʱ +1,16,0,0,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(ǰ)A෴йʱ +1,17,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)A޹1ʱ +1,18,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)A޹2ʱ +1,19,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)Aһ޹ʱ +1,1A,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)Aڶ޹ʱ +1,1B,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)A޹ʱ +1,1C,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)A޹ʱ +1,1D,0,0,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(ǰ)Aʱ +1,1E,0,0,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(ǰ)A෴ʱ +1,29,0,0,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(ǰ)Bйʱ +1,2A,0,0,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(ǰ)B෴йʱ +1,2B,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)B޹1ʱ +1,2C,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)B޹2ʱ +1,2D,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)Bһ޹ʱ +1,2E,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)Bڶ޹ʱ +1,2F,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)B޹ʱ +1,30,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)B޹ʱ +1,31,0,0,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(ǰ)Bʱ +1,32,0,0,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(ǰ)B෴ʱ +1,3D,0,0,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(ǰ)Cйʱ +1,3E,0,0,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(ǰ)C෴йʱ +1,3F,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)C޹1ʱ +1,40,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)C޹2ʱ +1,41,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)Cһ޹ʱ +1,42,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)Cڶ޹ʱ +1,43,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)C޹ʱ +1,44,0,0,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(ǰ)C޹ʱ +1,45,0,0,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(ǰ)Cʱ +1,46,0,0,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(ǰ)C෴ʱ +1,1,0,1,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(1)йʱ +1,2,0,1,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(1)йʱ +1,3,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)޹1ʱ +1,4,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)޹2ʱ +1,5,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)һ޹ʱ +1,6,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)ڶ޹ʱ +1,7,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)޹ʱ +1,8,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)޹ʱ +1,9,0,1,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(1)ʱ +1,A,0,1,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(1)ʱ +1,15,0,1,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(1)Aйʱ +1,16,0,1,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(1)A෴йʱ +1,17,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)A޹1ʱ +1,18,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)A޹2ʱ +1,19,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)Aһ޹ʱ +1,1A,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)Aڶ޹ʱ +1,1B,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)A޹ʱ +1,1C,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)A޹ʱ +1,1D,0,1,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(1)Aʱ +1,1E,0,1,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(1)A෴ʱ +1,29,0,1,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(1)Bйʱ +1,2A,0,1,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(1)B෴йʱ +1,2B,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)B޹1ʱ +1,2C,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)B޹2ʱ +1,2D,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)Bһ޹ʱ +1,2E,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)Bڶ޹ʱ +1,2F,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)B޹ʱ +1,30,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)B޹ʱ +1,31,0,1,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(1)Bʱ +1,32,0,1,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(1)B෴ʱ +1,3D,0,1,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(1)Cйʱ +1,3E,0,1,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(1)C෴йʱ +1,3F,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)C޹1ʱ +1,40,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)C޹2ʱ +1,41,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)Cһ޹ʱ +1,42,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)Cڶ޹ʱ +1,43,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)C޹ʱ +1,44,0,1,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(1)C޹ʱ +1,45,0,1,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(1)Cʱ +1,46,0,1,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(1)C෴ʱ +1,1,0,C,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(12)йʱ +1,2,0,C,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(12)йʱ +1,3,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)޹1ʱ +1,4,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)޹2ʱ +1,5,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)һ޹ʱ +1,6,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)ڶ޹ʱ +1,7,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)޹ʱ +1,8,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)޹ʱ +1,9,0,C,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(12)ʱ +1,A,0,C,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(12)ʱ +1,15,0,C,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(12)Aйʱ +1,16,0,C,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(12)A෴йʱ +1,17,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)A޹1ʱ +1,18,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)A޹2ʱ +1,19,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)Aһ޹ʱ +1,1A,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)Aڶ޹ʱ +1,1B,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)A޹ʱ +1,1C,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)A޹ʱ +1,1D,0,C,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(12)Aʱ +1,1E,0,C,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(12)A෴ʱ +1,29,0,C,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(12)Bйʱ +1,2A,0,C,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(12)B෴йʱ +1,2B,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)B޹1ʱ +1,2C,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)B޹2ʱ +1,2D,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)Bһ޹ʱ +1,2E,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)Bڶ޹ʱ +1,2F,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)B޹ʱ +1,30,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)B޹ʱ +1,31,0,C,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(12)Bʱ +1,32,0,C,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(12)B෴ʱ +1,3D,0,C,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(12)Cйʱ +1,3E,0,C,XX.XXXX|YYMMDDhhmm,8,kW,TRUE,FALSE,(12)C෴йʱ +1,3F,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)C޹1ʱ +1,40,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)C޹2ʱ +1,41,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)Cһ޹ʱ +1,42,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)Cڶ޹ʱ +1,43,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)C޹ʱ +1,44,0,C,XX.XXXX|YYMMDDhhmm,8,kvar,TRUE,FALSE,(12)C޹ʱ +1,45,0,C,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(12)Cʱ +1,46,0,C,XX.XXXX|YYMMDDhhmm,8,kVA,TRUE,FALSE,(12)C෴ʱ +2,1,1,0,XXX.X,2,V,TRUE,FALSE,Aѹ +2,1,2,0,XXX.X,2,V,TRUE,FALSE,Bѹ +2,1,3,0,XXX.X,2,V,TRUE,FALSE,Cѹ +2,2,1,0,XXX.XXX,3,A,TRUE,FALSE,A +2,2,2,0,XXX.XXX,3,A,TRUE,FALSE,B +2,2,3,0,XXX.XXX,3,A,TRUE,FALSE,C +2,3,0,0,XXX.XXX,3,kW,TRUE,FALSE,˲ʱй +2,3,1,0,XXX.XXX,3,kW,TRUE,FALSE,˲ʱAй +2,3,2,0,XXX.XXX,3,kW,TRUE,FALSE,˲ʱBй +2,3,3,0,XXX.XXX,3,kW,TRUE,FALSE,˲ʱCй +2,4,0,0,XXX.XXX,3,kvar ,TRUE,FALSE,˲ʱ޹ +2,4,1,0,XXX.XXX,3,kvar ,TRUE,FALSE,˲ʱA޹ +2,4,2,0,XXX.XXX,3,kvar ,TRUE,FALSE,˲ʱB޹ +2,4,3,0,XXX.XXX,3,kvar ,TRUE,FALSE,˲ʱC޹ +2,5,0,0,XXX.XXX,3,kVA,TRUE,FALSE,˲ʱڹ +2,5,1,0,XXX.XXX,3,kVA,TRUE,FALSE,˲ʱAڹ +2,5,2,0,XXX.XXX,3,kVA,TRUE,FALSE,˲ʱBڹ +2,5,3,0,XXX.XXX,3,kVA,TRUE,FALSE,˲ʱCڹ +2,6,0,0,X.XXX,2,kVA,TRUE,FALSE,ܹ +2,6,1,0,X.XXX,2,kVA,TRUE,FALSE,A๦ +2,6,2,0,X.XXX,2,kVA,TRUE,FALSE,B๦ +2,6,3,0,X.XXX,2,kVA,TRUE,FALSE,C๦ +2,7,1,0,X.XXX,2,,TRUE,FALSE,A +2,7,2,0,X.XXX,2,,TRUE,FALSE,B +2,7,3,0,X.XXX,2,,TRUE,FALSE,C +2,8,1,0,X.XXX,2,% ,TRUE,FALSE,Aѹʧ +2,8,2,0,X.XXX,2,% ,TRUE,FALSE,Bѹʧ +2,8,3,0,X.XXX,2,% ,TRUE,FALSE,Cѹʧ +2,9,1,0,X.XXX,2,% ,TRUE,FALSE,Aʧ +2,9,2,0,X.XXX,2,% ,TRUE,FALSE,Bʧ +2,9,3,0,X.XXX,2,% ,TRUE,FALSE,Cʧ +2,A,1,0,X.XXX,2,% ,TRUE,FALSE,Aѹ1г +2,A,2,0,X.XXX,2,% ,TRUE,FALSE,Bѹ1г +2,A,3,0,X.XXX,2,% ,TRUE,FALSE,Cѹ1г +2,B,1,0,X.XXX,2,% ,TRUE,FALSE,A1г +2,B,2,0,X.XXX,2,% ,TRUE,FALSE,B1г +2,B,3,0,X.XXX,2,% ,TRUE,FALSE,C1г +2,80,0,1,XXX.XXX,3,A,TRUE,FALSE,ߵ +2,80,0,2,XX.XX,2,Hz,TRUE,FALSE,Ƶ +2,80,0,3,XX.XXXX,3,kW,TRUE,FALSE,һйƽ +2,80,0,4,XX.XXXX,3,kW,TRUE,FALSE,ǰй +2,80,0,5,XX.XXXX,3,kvar,TRUE,FALSE,ǰ޹ +2,80,0,6,XX.XXXX,3,kVA,TRUE,FALSE,ǰ +2,80,0,7,XXX.X,2,,TRUE,FALSE,¶ +2,80,0,8,XX.XX,2,V,TRUE,FALSE,ʱӵصѹ(ڲ) +2,80,0,9,XX.XX,2,V,TRUE,FALSE,ͣ糭صѹ(ⲿ) +2,80,0,0A,XXXXXXXX,4,,TRUE,FALSE,ڲعʱ +2,80,0,0B,XXXX.XXXX,4,Ԫ/kWh,TRUE,FALSE,ǰݵ +4,0,1,1,YYMMDDWW,4,,TRUE,TRUE,ڼ(0) +4,0,1,2,hhmmss,3,ʱ,TRUE,TRUE,ʱ +4,0,1,3,NN,1,,TRUE,TRUE, +4,0,1,4,NN,1,,TRUE,TRUE,ʱ +4,0,1,5,XXXX,2,,TRUE,TRUE,У +4,0,1,6,YYMMDDhhmm,5,ʱ,TRUE,TRUE,ʱлʱ +4,0,1,7,YYMMDDhhmm,5,ʱ,TRUE,TRUE,ʱαлʱ +4,0,1,8,YYMMDDhhmm,5,ʱ,TRUE,TRUE,׷ʵлʱ +4,0,1,9,YYMMDDhhmm,5,ʱ,TRUE,TRUE,׽лʱ +4,0,2,1,NN,1,,TRUE,TRUE,ʱp14 +4,0,2,2,NN,1,,TRUE,TRUE,ʱαq8 +4,0,2,3,NN,1,,TRUE,TRUE,ʱ(ÿл)m14 +4,0,2,4,NN,1,,TRUE,TRUE,k63 +4,0,2,5,NNNN,2,,TRUE,TRUE,n254 +4,0,2,6,NN,1,,TRUE,TRUE,г +4,0,2,7,NN,1,,TRUE,TRUE, +4,0,3,1,NN,1,,TRUE,TRUE,Զѭʾ +4,0,3,2,NN,1,,TRUE,TRUE,ÿʾʱ +4,0,3,3,NN,1,λ,TRUE,TRUE,ʾСλ +4,0,3,4,NN,1,λ,TRUE,TRUE,ʾ()Сλ +4,0,3,5,NN,1,,TRUE,TRUE,ѭʾ +4,0,3,6,NNNNNN,3,,TRUE,TRUE, +4,0,3,7,NNNNNN,3,,TRUE,TRUE,ѹ +4,0,4,1,NNNNNNNNNNNN,6,,TRUE,TRUE,ͨŵַ +4,0,4,2,NNNNNNNNNNNN,6,,TRUE,TRUE, +4,0,4,3,NN...NN,32,,TRUE,TRUE,ʲ(ASCII) +4,0,4,4,XXXXXXXXXXXX,6,,TRUE,FALSE,ѹ(ASCII) +4,0,4,5,XXXXXXXXXXXX,6,,TRUE,FALSE,/(ASCII) +4,0,4,6,XXXXXXXXXXXX,6,,TRUE,FALSE,(ASCII) +4,0,4,7,XXXXXXXX,4,,TRUE,FALSE,й׼ȷȵȼ(ASCII) +4,0,4,8,XXXXXXXX,4,,TRUE,FALSE,޹׼ȷȵȼ(ASCII) +4,0,4,9,XXXXXX,3,imp/kWh,TRUE,FALSE,й +4,0,4,0A,XXXXXX,3,imp/kvarh,TRUE,FALSE,޹ +4,0,4,0B,XX...XX,10,,TRUE,FALSE,ͺ(ASCII) +4,0,4,0C,XX...XX,10,,TRUE,FALSE,(ASCII) +4,0,4,0D,XX...XX,16,,TRUE,FALSE,Э汾(ASCII) +4,0,4,0E,NNNNNNNNNNNN,6,,TRUE,FALSE,ͻ +4,0,5,1,XXXX,2,,TRUE,FALSE,״̬1 +4,0,6,1,NN,1,,TRUE,TRUE,йϷʽ +4,0,6,2,NN,1,,TRUE,TRUE,޹Ϸʽ1 +4,0,6,3,NN,1,,TRUE,TRUE,޹Ϸʽ2 +4,0,7,1,NN,1,,TRUE,TRUE,ͺͨ +4,0,7,2,NN,1,,TRUE,TRUE,Ӵʽͨ +4,0,7,3,NN,1,,TRUE,TRUE,ͨſ1ͨ +4,0,7,4,NN,1,,TRUE,TRUE,ͨſ2ͨ +4,0,7,5,NN,1,,TRUE,TRUE,ͨſ3ͨ +4,0,8,1,NN,1,,TRUE,TRUE, +4,0,8,2,NN,1,,TRUE,TRUE,ղõʱα +4,0,9,1,NN,1,,TRUE,TRUE,ɼ¼ģʽ +4,0,9,2,NN,1,,TRUE,TRUE,ʱģʽ +4,0,9,3,NN,1,,TRUE,TRUE,˲ʱģʽ +4,0,9,4,NN,1,,TRUE,TRUE,Լģʽ +4,0,9,5,NN,1,,TRUE,TRUE,㶳ģʽ +4,0,9,6,NN,1,,TRUE,TRUE,նģʽ +4,0,A,1,MMDDhhmm,4,,TRUE,TRUE,ɼ¼ʼʱ +4,0,B,1,DDhh,2,,TRUE,TRUE,ÿµ1 +4,0,B,2,DDhh,2,,TRUE,TRUE,ÿµ2 +4,0,B,3,DDhh,2,,TRUE,TRUE,ÿµ3 +4,0,C,1,NNNNNNNN,4,,FALSE,TRUE,0 +4,0,D,1,N.NNN,2,,TRUE,TRUE,A絼ϵ +4,0,D,2,N.NNN,2,,TRUE,TRUE,Aϵ +4,0,D,3,N.NNN,2,,TRUE,TRUE,Aϵ +4,0,D,4,N.NNN,2,,TRUE,TRUE,A翹ϵ +4,0,D,5,N.NNN,2,,TRUE,TRUE,B絼ϵ +4,0,D,6,N.NNN,2,,TRUE,TRUE,Bϵ +4,0,D,7,N.NNN,2,,TRUE,TRUE,Bϵ +4,0,D,8,N.NNN,2,,TRUE,TRUE,B翹ϵ +4,0,D,9,N.NNN,2,,TRUE,TRUE,C絼ϵ +4,0,D,0A,N.NNN,2,,TRUE,TRUE,Cϵ +4,0,D,0B,N.NNN,2,,TRUE,TRUE,Cϵ +4,0,D,0C,N.NNN,2,,TRUE,TRUE,C翹ϵ +4,0,E,1,NN.NNNN,3,kW,TRUE,TRUE,йֵ +4,0,E,2,NN.NNNN,3,kW,TRUE,TRUE,йֵ +4,0,E,3,NNN.N,2,V,TRUE,TRUE,ѹֵ +4,0,E,4,NNN.N,2,V,TRUE,TRUE,ѹֵ +4,0,F,1,XXXXXX.XX,4,kWh,TRUE,TRUE,1ֵ +4,0,F,2,XXXXXX.XX,4,kWh,TRUE,TRUE,2ֵ +4,0,F,3,XXXXXX.XX,4,kWh,TRUE,TRUE,ڻֵ +4,0,F,4,XXXXXX.XX,4,kWh,TRUE,TRUE,͸ֵ֧ +4,0,10,1,XXXXXX.XX,4,Ԫ,TRUE,TRUE,1ֵ +4,0,10,2,XXXXXX.XX,4,Ԫ,TRUE,TRUE,2ֵ +4,0,10,3,XXXXXX.XX,4,Ԫ,TRUE,TRUE,͸ֵ֧ +4,0,10,4,NNNNNN.NN,4,Ԫ,TRUE,TRUE,ڻֵ +4,0,10,5,NNNNNN.NN,4,Ԫ,TRUE,TRUE,բֵ +4,0,12,1,YYMMDDhhmm,5,ʱ,TRUE,TRUE,㶳ʼʱ +4,0,12,2,NN,1,,TRUE,TRUE,㶳ʱ +4,0,12,3,hhmm,2,ʱ,TRUE,TRUE,նʱ +4,0,13,1,NN,1,,TRUE,TRUE,ͨ߼źǿָʾ +4,0,14,1,NNNN,2,,TRUE,TRUE,բʱʱ䣨NNNNΪբǰ澯ʱ䣩 +4,80,0,1,NN...NN,32,,TRUE,FALSE,汾(ASCII) +4,80,0,2,NN...NN,33,,TRUE,FALSE,Ӳ汾(ASCII) +4,80,0,3,NN...NN,34,,TRUE,FALSE,ұ(ASCII) +4,9,1,1,NNN.N,2,V,TRUE,TRUE,ʧѹ¼ѹ +4,9,1,2,NNN.N,2,V,TRUE,TRUE,ʧѹ¼ѹָ +4,9,1,3,NN.NNNN,3,A,TRUE,TRUE,ʧѹ¼ +4,9,1,4,NN,1,,TRUE,TRUE,ʧѹ¼жʱʱ +4,9,2,1,NNN.N,2,V,TRUE,TRUE,Ƿѹ¼ѹ +4,9,2,2,NN,1,,TRUE,TRUE,Ƿѹ¼жʱʱ +4,9,3,1,NNN.N,2,V,TRUE,TRUE,ѹ¼ѹ +4,9,3,2,NN,1,,TRUE,TRUE,ѹ¼жʱʱ \ No newline at end of file diff --git a/DLT645-plugin/src/main/resources/application.yml b/DLT645-plugin/src/main/resources/application.yml new file mode 100644 index 0000000..c29e15f --- /dev/null +++ b/DLT645-plugin/src/main/resources/application.yml @@ -0,0 +1,8 @@ +plugin: + runMode: prod + mainPackage: cc.iotkit.plugin + +tcp: + host: 25on621889.goho.co + port: 43161 + interval: 10000 diff --git a/DLT645-plugin/src/main/resources/config.json b/DLT645-plugin/src/main/resources/config.json new file mode 100644 index 0000000..2651447 --- /dev/null +++ b/DLT645-plugin/src/main/resources/config.json @@ -0,0 +1,23 @@ +[ + { + "id": "host", + "name": "服务端ip", + "type": "text", + "value": "25on621889.goho.co", + "desc": "服务端ip" + }, + { + "id": "port", + "name": "服务端端口", + "type": "number", + "value": 43161, + "desc": "服务端端口" + }, + { + "id": "interval", + "name": "采集频率", + "type": "number", + "value": 10000, + "desc": "采集频率" + } +] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..983848e --- /dev/null +++ b/README.en.md @@ -0,0 +1,36 @@ +# iot-iita-plugins + +#### Description +官方插件示例仓库 + +#### Software Architecture +Software architecture description + +#### Installation + +1. xxxx +2. xxxx +3. xxxx + +#### Instructions + +1. xxxx +2. xxxx +3. xxxx + +#### Contribution + +1. Fork the repository +2. Create Feat_xxx branch +3. Commit your code +4. Create Pull Request + + +#### Gitee Feature + +1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md +2. Gitee blog [blog.gitee.com](https://blog.gitee.com) +3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) +4. The most valuable open source project [GVP](https://gitee.com/gvp) +5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) +6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee97024 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# iot-iita-plugins + +#### 介绍 +官方插件示例仓库 + +#### 软件架构 +软件架构说明 + + +#### 安装教程 + +1. xxxx +2. xxxx +3. xxxx + +#### 使用说明 + +1. xxxx +2. xxxx +3. xxxx + +#### 参与贡献 + +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 +4. 新建 Pull Request + + +#### 特技 + +1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md +2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) +3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 +4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 +5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) +6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/emqx-plugin/pom.xml b/emqx-plugin/pom.xml new file mode 100644 index 0000000..5f5b31a --- /dev/null +++ b/emqx-plugin/pom.xml @@ -0,0 +1,86 @@ + + + + iot-iita-plugins + cc.iotkit.plugins + 2.10.19 + + 4.0.0 + + emqx-plugin + + + + + io.vertx + vertx-core + ${vertx.version} + + + + io.vertx + vertx-mqtt + ${vertx.version} + + + + io.vertx + vertx-web-proxy + ${vertx.version} + + + + + + + dev + + true + + + dev + + + + + prod + + prod + + + + + + + + com.gitee.starblues + spring-brick-maven-packager + ${spring-brick.version} + + ${plugin.build.mode} + + emqx-plugin + cc.iotkit.plugins.emqx.Application + ${project.version} + iita + emqx示例插件 + application.yml + + + jar + + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/Application.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/Application.java new file mode 100644 index 0000000..19b1031 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/Application.java @@ -0,0 +1,19 @@ +package cc.iotkit.plugins.emqx; + +import com.gitee.starblues.bootstrap.SpringPluginBootstrap; +import com.gitee.starblues.bootstrap.annotation.OneselfConfig; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * @author sjg + */ +@SpringBootApplication(scanBasePackages = "cc.iotkit.plugins.emqx") +@OneselfConfig(mainConfigFileName = {"application.yml"}) +@EnableConfigurationProperties +public class Application extends SpringPluginBootstrap { + + public static void main(String[] args) { + new Application().run(Application.class, args); + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/BCDClockGenerator.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/BCDClockGenerator.java new file mode 100644 index 0000000..4d48461 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/BCDClockGenerator.java @@ -0,0 +1,39 @@ +package cc.iotkit.plugins.emqx.conf; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class BCDClockGenerator { + + // BCD编码映射表(0-9) + private static final byte[] BCD_TABLE = { + 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09 + }; + + public static byte[] generateBCDTime() { + LocalDateTime now = LocalDateTime.now(); + String timeStr = now.format(DateTimeFormatter.ofPattern("yyMMddHHmmss")); + + byte[] bcdBytes = new byte[6]; // 6字节存储YYMMDDhhmmss + for (int i = 0; i < 6; i++) { + int high = Character.digit(timeStr.charAt(i*2), 10); + int low = Character.digit(timeStr.charAt(i*2+1), 10); + bcdBytes[i] = (byte) ((BCD_TABLE[high] << 4) | BCD_TABLE[low]); + } + return bcdBytes; + } + + public static String formatBCDToHex(byte[] bcd) { + StringBuilder sb = new StringBuilder(); + for (byte b : bcd) { + sb.append(String.format("%02X ", b)); + } + return sb.toString().replaceAll("\\s+", ""); + } + + /*public static void main(String[] args) { + byte[] bcdTime = generateBCDTime(); + System.out.println(formatBCDToHex(bcdTime)); + }*/ +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/BeanConfig.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/BeanConfig.java new file mode 100644 index 0000000..2b8d0a4 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/BeanConfig.java @@ -0,0 +1,28 @@ +package cc.iotkit.plugins.emqx.conf; + +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.LocalPluginConfig; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugins.emqx.service.FakeThingService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @author sjg + */ +@Component +public class BeanConfig { + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IThingService getThingService() { + return new FakeThingService(); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginConfig getPluginConfig(){ + return new LocalPluginConfig(); + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/CRC16ModbusUtil.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/CRC16ModbusUtil.java new file mode 100644 index 0000000..cbb875c --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/CRC16ModbusUtil.java @@ -0,0 +1,64 @@ +package cc.iotkit.plugins.emqx.conf; + +public class CRC16ModbusUtil { + private static final int POLY = 0xA001; + private static final int INIT_VALUE = 0xFFFF; + + public static String calculate(String hexStr) { + byte[] data = hexToByteArray(hexStr); + int crc = calculateCrc(data); + return String.format("%04X", crc); + } + + private static int calculateCrc(byte[] data) { + int crc = INIT_VALUE; + for (byte b : data) { + crc ^= (b & 0xFF); + for (int i = 0; i < 8; i++) { + boolean lsb = (crc & 1) == 1; + crc >>>= 1; + if (lsb) crc ^= POLY; + } + } + return crc; + } + + private static byte[] hexToByteArray(String hexStr) { + if (hexStr.length() % 2 != 0) { + throw new IllegalArgumentException("Hex string must have even length"); + } + byte[] bytes = new byte[hexStr.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + String byteStr = hexStr.substring(2*i, 2*i+2); + bytes[i] = (byte) Integer.parseInt(byteStr, 16); + } + return bytes; + } + /** + *3.1 帧结构 + * 字段名 长度 代号 备注 + * 帧头 1 HEAD 固定为0xaa + * 协议类型 1 TYPE + * 协议版本 1 VERSION + * 帧长度 2 LENGTH + * 消息序号 1 MID + * 命令码 1 CMD + * 数据域 N DATA + * 帧校验 2 CRC 采用CRC16-MODBUS校验方式 + * 帧尾 1 TAIL 固定为0x55 + * + * */ + public static String buildFrame( + String type, String version, + String mid, String cmd, String dataHex + ) { + String headerLength = "AA" + type + version + mid + cmd + dataHex +"crc1"+ "55"; + Integer length =headerLength.length()/2; + String header = "AA" + type + version + length + mid + cmd; + String crcData = length + mid + cmd + dataHex; + String crc = calculate(crcData); + return header + dataHex + crc + "55"; + } + + +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/DeviceStatusParser.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/DeviceStatusParser.java new file mode 100644 index 0000000..20f9e9a --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/DeviceStatusParser.java @@ -0,0 +1,99 @@ +package cc.iotkit.plugins.emqx.conf; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cc.iotkit.common.utils.HexUtil.hexStringToByteArray; + +public class DeviceStatusParser { + + private static final int COMMAND_CODE = 0x11; + public static Map parseStatusReportToMap(String hexData) { + byte[] data = GasAlarmDataParser.hexStringToByteArray(hexData); + return parseStatusReport(data); + } + public static Map parseStatusReport(byte[] hexData) { + Map result = new HashMap<>(); + ByteBuffer buffer = ByteBuffer.wrap(hexData); + + // 验证命令码 + /* int cmd = buffer.get() & 0xFF; + if (cmd != COMMAND_CODE) { + throw new IllegalArgumentException("Invalid command code: 0x" + Integer.toHexString(cmd)); + }*/ + + // 解析终端类型 (2字节) + int deviceType = buffer.getShort() & 0xFFFF; + result.put("deviceType", deviceType); + + // 解析设备编号IMEI (15字节) 对应程序的 + byte[] imeiBytes = new byte[15]; + buffer.get(imeiBytes); + result.put("imei", new String(imeiBytes).trim()); + + // 解析ICCID卡号 (20字节) + byte[] iccidBytes = new byte[20]; + buffer.get(iccidBytes); + result.put("iccid", new String(iccidBytes).trim()); + + // 解析软件版本 (4字节) + byte[] versionBytes = new byte[4]; + buffer.get(versionBytes); + result.put("version", bytesToVersion(versionBytes)); + + // 解析通信模式 (1字节) + int commMode = buffer.get() & 0xFF; + result.put("communicationMode", commMode); + + // 解析信号强度 (1字节) + int signal = buffer.get() & 0xFF; + result.put("signalStrength", signal); + + return result; + } + + private static String getDeviceTypeName(int type) { + switch (type) { + case 0: return "家用报警器"; + case 1: return "独立式报警器"; + case 2: return "工商业控制器"; + case 3: return "动火离人设备"; + default: return "未知设备类型"; + } + } + + private static String getCommModeName(int mode) { + switch (mode) { + case 0: return "NB-IoT"; + case 1: return "4G"; + case 2: return "WiFi"; + default: return "未知通信模式"; + } + } + + private static String bytesToVersion(byte[] bytes) { + return String.format("%d.%d.%d.%d", + bytes[0] & 0xFF, + bytes[1] & 0xFF, + bytes[2] & 0xFF, + bytes[3] & 0xFF); + } + + public static void main(String[] args) { + // 示例16进制数据 (命令码0x11 + 测试数据) + byte[] testData = new byte[] { + // 0x11, // 命令码 + 0x00, 0x02, // 工商业控制器 + '8','6','1','2','3','4','0','0','0','0','1','2','3','4','5', // IMEI + '8','9','8','6','0','0','0','0','0','0','0','0','0','0','0','1','2','3','4','5', // ICCID + 0x01, 0x02, 0x03, 0x04, // 软件版本1.2.3.4 + 0x01, // 4G通信 + 0x5A,0X5A // 信号强度90 + }; + + Map result = DeviceStatusParser.parseStatusReport(testData); + System.out.println("解析结果: " + result); + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/EventTypeMapper.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/EventTypeMapper.java new file mode 100644 index 0000000..6bb2980 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/EventTypeMapper.java @@ -0,0 +1,53 @@ +package cc.iotkit.plugins.emqx.conf; + +import java.util.HashMap; +import java.util.Map; + +public class EventTypeMapper { + private static final Map EVENT_TYPE_MAP = new HashMap<>(); + + static { + // 节点相关事件 + EVENT_TYPE_MAP.put(1, "首警高"); + EVENT_TYPE_MAP.put(2, "首警低"); + EVENT_TYPE_MAP.put(3, "报警高"); + EVENT_TYPE_MAP.put(4, "报警低"); + EVENT_TYPE_MAP.put(5, "低限报警恢复"); + EVENT_TYPE_MAP.put(6, "高限报警恢复"); + EVENT_TYPE_MAP.put(7, "探头传感器故障"); + EVENT_TYPE_MAP.put(8, "节点通讯断网故障"); + EVENT_TYPE_MAP.put(9, "探头传感器故障恢复"); + EVENT_TYPE_MAP.put(10, "节点通讯断网故障恢复"); + EVENT_TYPE_MAP.put(11, "自检/模块联动"); + EVENT_TYPE_MAP.put(12, "阀门动作"); + + // 控制器相关事件 + EVENT_TYPE_MAP.put(13, "自检"); + EVENT_TYPE_MAP.put(14, "备电故障"); + EVENT_TYPE_MAP.put(15, "主电故障"); + EVENT_TYPE_MAP.put(16, "主电欠压"); + EVENT_TYPE_MAP.put(17, "控制器复位"); + EVENT_TYPE_MAP.put(18, "控制器开机"); + EVENT_TYPE_MAP.put(19, "主电故障恢复"); + EVENT_TYPE_MAP.put(20, "备电故障恢复"); + EVENT_TYPE_MAP.put(21, "主电欠压恢复"); + + // 动火离人相关事件 + EVENT_TYPE_MAP.put(22, "设备开机"); + EVENT_TYPE_MAP.put(23, "动火离人报警"); + EVENT_TYPE_MAP.put(24, "动火离人报警恢复"); + EVENT_TYPE_MAP.put(25, "设备故障"); + EVENT_TYPE_MAP.put(26, "设备故障恢复"); + EVENT_TYPE_MAP.put(27, "断网故障"); + EVENT_TYPE_MAP.put(28, "断网故障恢复"); + + // 正常上报事件 + EVENT_TYPE_MAP.put(128, "燃气报警器正常数据上报"); + EVENT_TYPE_MAP.put(129, "动火离人正常数据上报"); + } + + public static String getEventTypeName(int eventType) { + return EVENT_TYPE_MAP.getOrDefault(eventType, "未知事件类型"); + } + +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/GasAlarmDataParser.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/GasAlarmDataParser.java new file mode 100644 index 0000000..6a6a3d8 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/GasAlarmDataParser.java @@ -0,0 +1,201 @@ +package cc.iotkit.plugins.emqx.conf; + +import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; +import java.util.*; + +public class GasAlarmDataParser { + public static void main(String[] args) { + // 节点相关事件测试数据 (1-12) + // 节点相关事件测试数据 (1-12) - 数值部分改为低位在前 + String[] nodeEvents = { + "00012025052710253000010002", // 1-首警高 (数值0001→0100) + "00022025052710253000020003", // 2-首警低 + "00032025052710253000030004", // 3-报警高 + "00042025052710253000040005", // 4-报警低 + "00052025052710253000050006", // 5-低限报警恢复 + "00062025052710253000060007", // 6-高限报警恢复 + "00072025052710253000070008", // 7-探头传感器故障 + "00082025052710253000080009", // 8-节点通讯断网故障 + "0009202505271025300009000A", // 9-探头传感器故障恢复 + "000A20250527102530000A000B", // 10-节点通讯断网故障恢复 + "000B20250527102530000B000C", // 11-自检/模块联动 + "000C20250527102530000C000D" // 12-阀门动作 + }; + +// 控制器相关事件测试数据 (13-21) - 时间部分保持原样 + String[] controllerEvents = { + "000D20250527102530", // 13-自检 + "000E20250527102530", // 14-备电故障 + "000F20250527102530", // 15-主电故障 + "001020250527102530", // 16-主电欠压 + "001120250527102530", // 17-控制器复位 + "001220250527102530", // 18-控制器开机 + "001320250527102530", // 19-主电故障恢复 + "001420250527102530", // 20-备电故障恢复 + "001520250527102530" // 21-主电欠压恢复 + }; + +// 动火离人相关事件测试数据 (22-28) + String[] fireMonitorEvents = { + "00162025052710253000010002", // 22-设备开机 + "00172025052710253000020003", // 23-动火离人报警 + "00182025052710253000030004", // 24-动火离人报警恢复 + "00192025052710253000040005", // 25-设备故障 + "001A2025052710253000050006", // 26-设备故障恢复 + "001B2025052710253000060007", // 27-断网故障 + "001C2025052710253000070008" // 28-断网故障恢复 + }; + +// 正常数据上报测试数据 (128-129) + String[] normalReports = { + "0080202505271025300003000200010203040005000600070008", // 128-燃气报警器 + "01812025052710253000020001000102030400050006" // 129-动火离人 + }; + + testEventGroup("节点事件", nodeEvents); + testEventGroup("控制器事件", controllerEvents); + testEventGroup("动火离人事件", fireMonitorEvents); + testEventGroup("正常上报", normalReports); + } + + private static void testEventGroup(String groupName, String[] testCases) { + System.out.println("\n=== " + groupName + "测试 ==="); + for (String hexData : testCases) { + try { + List> result = GasAlarmDataParser.parseHexDataToList(hexData); + System.out.println("原始数据: " + hexData); + System.out.println("解析结果: " + result.get(0).get("eventType") + + " - " + result.get(0).get("dataType")); + System.out.println("rrr解析结果: " + result.get(0)); + + + } catch (Exception e) { + System.out.println("解析异常: " + e.getMessage()); + } + } + } + public static List> parseHexDataToList(String hexData) { + byte[] data = hexStringToByteArray(hexData); + return parseDataToList(data); + } + + public static List> parseDataToList(byte[] data) { + List> resultList = new ArrayList<>(); + if (data == null || data.length < 1) { + return resultList; + } + + int eventType = Byte.toUnsignedInt(data[0]) + Byte.toUnsignedInt(data[1]); + if (eventType == 128) { + // // 正常数据上报 (NORMAL_DATA) + //4.2.3 终端上报连接节点数据域格式 + //此数据域主要用于当“家用报警器”、“独立式报警器”和“工商业控制器”定时上报数据。当设备首次上电时,也发送此数据域数据。 + return parseNormalDataToList(data); + } else if (isControllerEvent(eventType)) { + // 4.2.1 终端上报控制器事件数据域格式 + // 控制器事件 (CONTROLLER_EVENT) + return parseControllerEventToList(data); + } + //4.2.2 终端上报节点事件数据域格式 + // 节点事件 (NODE_EVENT) + return parseNodeEventToList(data); + } + + private static List> parseControllerEventToList(byte[] data) { + List> eventList = new ArrayList<>(); + ByteBuffer buffer = ByteBuffer.wrap(data); + Map eventMap = new LinkedHashMap<>(); + + eventMap.put("dataType", "CONTROLLER_EVENT"); + eventMap.put("eventType", buffer.getShort() & 0xFFFF); + eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(Integer.parseInt(eventMap.get("eventType").toString()))); + eventMap.put("timestamp", parseBcdTime(buffer)); + eventList.add(eventMap); + + return eventList; + } + + private static List> parseNodeEventToList(byte[] data) { + List> eventList = new ArrayList<>(); + ByteBuffer buffer = ByteBuffer.wrap(data); + Map eventMap = new LinkedHashMap<>(); + + eventMap.put("dataType", "NODE_EVENT"); + eventMap.put("eventType", buffer.getShort() & 0xFFFF); + eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(Integer.parseInt(eventMap.get("eventType").toString()))); + eventMap.put("timestamp", parseBcdTime(buffer)); + eventMap.put("nodeId", buffer.getShort() & 0xFFFF); + eventMap.put("dataValue", buffer.getShort() & 0xFFFF); + eventList.add(eventMap); + + return eventList; + } + + private static List> parseNormalDataToList(byte[] data) { + List> dataList = new ArrayList<>(); + ByteBuffer buffer = ByteBuffer.wrap(data); + + // 基础信息 + Map baseInfo = new LinkedHashMap<>(); + baseInfo.put("dataType", "NORMAL_DATA"); + baseInfo.put("eventType", buffer.getShort() & 0xFFFF); + baseInfo.put("eventTypeValue", EventTypeMapper.getEventTypeName(Integer.parseInt(baseInfo.get("eventType").toString()))); + baseInfo.put("timestamp", parseBcdTime(buffer)); + baseInfo.put("totalNodes", buffer.getShort() & 0xFFFF); + baseInfo.put("reportedNodes", buffer.get() & 0xFF); + dataList.add(baseInfo); + + // 节点数据 + while (buffer.remaining() >= 8) { + Map nodeMap = new LinkedHashMap<>(); + nodeMap.put("nodeId", buffer.getShort() & 0xFFFF); + nodeMap.put("nodeType", buffer.get() & 0xFF); + nodeMap.put("unit", buffer.get() & 0xFF); + nodeMap.put("precision", buffer.get() & 0xFF); + nodeMap.put("gasType", buffer.get() & 0xFF); + nodeMap.put("dataValue", buffer.getShort() & 0xFFFF); + if(nodeMap.get("nodeType").equals("0")){ + // 0:节点为探测器 + nodeMap.put("dataTypeValue", nodeMap.get("dataValue")); + }else if (nodeMap.get("nodeType").equals("1")){ + //1:节点为输出模块 + nodeMap.put("dataTypeValue",nodeMap.get("dataValue").equals("0")?"未动作":"已动作"); + } + dataList.add(nodeMap); + } + + return dataList; + } + + // 保留原有辅助方法 + private static String parseBcdTime(ByteBuffer buffer) { + byte[] bcdTime = new byte[6]; + buffer.get(bcdTime); + StringBuilder sb = new StringBuilder(); + for (byte b : bcdTime) { + sb.append(String.format("%02X", b)); + } + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmss"); + Date date = sdf.parse(sb.toString()); + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); + } catch (Exception e) { + return sb.toString(); + } + } + + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + private static boolean isControllerEvent(int eventType) { + return eventType >= 13 && eventType <= 21; + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/IoTConfigProtocol.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/IoTConfigProtocol.java new file mode 100644 index 0000000..85ad94c --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/IoTConfigProtocol.java @@ -0,0 +1,197 @@ +package cc.iotkit.plugins.emqx.conf; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +public class IoTConfigProtocol { + // 配置项常量定义(完整版) + public static final int IP_REALM = 0x0000; + public static final int PORT = 0x0001; + public static final int REPORT_AWAIT_TIME = 0x0002; + public static final int REPORT_RETRY_TIMES = 0x0003; + public static final int CARD = 0x0004; + public static final int USERNAME = 0x0010; + public static final int PASSWORD = 0x0011; + public static final int KEEP_ALIVE = 0x0012; + public static final int PUBLISH_TOPIC = 0x0016; + public static final int SUBSCRIBE_TOPIC = 0x0017; + public static final int REPORT_RATE_UPPER = 0x0021; + public static final int REPORT_CONCENTRATION_RANGE = 0x0022; + public static final int REPORT_RATE_LOWER = 0x0023; + public static final int SIGNAL = 0x0024; + public static final int TIME_SYNC = 0x0025; + public static final int VERSION = 0x0026; + // 生成读取指令 + public static String generateReadCommand(int terminalType, int configId) { + ByteBuffer buf = ByteBuffer.allocate(4); + // buf.putShort((short) 0xAA23); // 起始符+命令码 + // buf.putShort((short) 4); // 数据长度 + buf.putShort((short) terminalType); + buf.putShort((short) configId); + return bytesToHex(buf.array()); + } + + // 生成写入指令 + public static String generateWriteCommand(int terminalType, int configId, byte[] data) { + + ByteBuffer buf = ByteBuffer.allocate(5 + data.length); + //buf.putShort((short) (5 + data.length)); // 数据长度 + buf.putShort((short) terminalType); + buf.putShort((short) configId); + buf.put((byte) data.length); // 数据长度字节 + buf.put(data); // 实际数据 + + return bytesToHex(buf.array()); + } + // 完整读取响应解析 + public static Map parseReadResponse(String hexResp) { + byte[] data = hexToBytes(hexResp); + ByteBuffer buf = ByteBuffer.wrap(data); + + Map result = new HashMap<>(); + result.put("commandCode", buf.get() & 0xFF); + result.put("configId", buf.getShort() & 0xFFFF); + + int dataLen = buf.get() & 0xFF; + byte[] value = new byte[dataLen]; + buf.get(value); + + // 根据配置ID进行类型化解析 + switch((int)result.get("configId")) { + case IP_REALM: + case PUBLISH_TOPIC: + case SUBSCRIBE_TOPIC: + result.put("value", parseTLVString(value)); + break; + + case PORT: + case KEEP_ALIVE: + case REPORT_RATE_UPPER: + case REPORT_CONCENTRATION_RANGE: + case REPORT_RATE_LOWER: + result.put("value", ByteBuffer.wrap(value).getShort()); + break; + + case REPORT_AWAIT_TIME: + case REPORT_RETRY_TIMES: + case SIGNAL: + result.put("value", value[0] & 0xFF); + break; + + case CARD: + result.put("value", parseSimCard(value)); + break; + + case TIME_SYNC: + result.put("value", parseBcdTime(value)); + break; + + case VERSION: + result.put("value", parseVersion(value)); + break; + + default: + result.put("value", bytesToHex(value)); + } + return result; + } + + // 特殊格式解析方法 + private static String parseTLVString(byte[] data) { + int len = data[0] & 0xFF; + return new String(data, 1, Math.min(len, data.length-1), StandardCharsets.US_ASCII); + } + + private static String parseSimCard(byte[] data) { + int len = data[0] & 0xFF; + return new String(data, 1, Math.min(len, 20), StandardCharsets.US_ASCII); + } + + private static String parseBcdTime(byte[] data) { + return String.format("20%02d-%02d-%02d %02d:%02d:%02d", + bcdToInt(data[0]), bcdToInt(data[1]), bcdToInt(data[2]), + bcdToInt(data[3]), bcdToInt(data[4]), bcdToInt(data[5])); + } + + private static int bcdToInt(byte b) { + return ((b >> 4) & 0x0F)*10 + (b & 0x0F); + } + + private static String parseVersion(byte[] data) { + return String.format("%d.%d.%d.%d", + data[0] & 0xFF, data[1] & 0xFF, + data[2] & 0xFF, data[3] & 0xFF); + } + + // 字节数组与16进制转换工具 + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + } + return sb.toString(); + } + /* + * ip地址转换成TLV数据 + * */ + public static byte[] buildTLVData(byte[] value) { + byte[] data = new byte[value.length + 1]; + data[0] = (byte)value.length; + System.arraycopy(value, 0, data, 1, value.length); + return data; + } + /* + * 构建单字节数据 + * */ + public static byte[] buildByteData(int value) { + return new byte[] { (byte)(value & 0xFF) }; + } + // 构建字符串类型数据(带长度前缀) + public static byte[] buildStringData(String value, int maxLength) { + byte[] strBytes = value.getBytes(StandardCharsets.US_ASCII); + if(strBytes.length > maxLength) { + throw new IllegalArgumentException("Value exceeds max length"); + } + + byte[] data = new byte[strBytes.length+1]; + data[0] = (byte) strBytes.length; + System.arraycopy(strBytes, 0, data, 1, strBytes.length); + return data; + } + // 构建BCD时间数据(YYMMDDhhmmss) + public static byte[] buildBcdTimeData(String time) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMddHHmmss"); + String formatted = LocalDateTime.now().format(formatter); + + byte[] bcdData = new byte[6]; + for(int i = 0; i < 6; i++) { + int pair = Integer.parseInt(formatted.substring(i*2, i*2+2)); + bcdData[i] = (byte)(((pair / 10) << 4) | (pair % 10)); + } + return bcdData; + } + + // 构建版本号数据(4字节) +/* public static byte[] buildVersionData(String version) { + String[] parts = version.split("\\."); + if(parts.length != 4) throw new IllegalArgumentException(); + + byte[] data = new byte[4]; + for(int i = 0; i < 4; i++) { + data[i] = (byte)Integer.parseInt(parts[i]); + } + return data; + }*/ + private static byte[] hexToBytes(String hex) { + hex = hex.replaceAll("\\s", ""); + byte[] data = new byte[hex.length()/2]; + for (int i=0; i ipResult = IoTConfigProtocol.parseReadResponse(ipResponse); + System.out.println("IP解析结果: " + ipResult.get("value")); + + // 模拟端口响应 + String portResponse = "000001020383"; + Map portResult = IoTConfigProtocol.parseReadResponse(portResponse); + System.out.println("端口解析结果: " + portResult.get("value")); + + // 模拟SIM卡响应 + String cardResponse = "00001800040015464347413230313233343536373839303132"; + Map cardResult = IoTConfigProtocol.parseReadResponse(cardResponse); + System.out.println("SIM卡解析结果: " + cardResult.get("value")); + + // 模拟时间响应 + String timeResponse = "00000D00250605143000"; + Map timeResult = IoTConfigProtocol.parseReadResponse(timeResponse); + System.out.println("时间解析结果: " + timeResult.get("value")); + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/handler/IMsgHandler.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/handler/IMsgHandler.java new file mode 100644 index 0000000..8b536e2 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/handler/IMsgHandler.java @@ -0,0 +1,12 @@ +package cc.iotkit.plugins.emqx.handler; + +import io.vertx.core.json.JsonObject; + +/** + * @author sjg + */ +public interface IMsgHandler { + + void handle(String topic, JsonObject payload); + +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/AuthVerticle.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/AuthVerticle.java new file mode 100644 index 0000000..757a54a --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/AuthVerticle.java @@ -0,0 +1,175 @@ +package cc.iotkit.plugins.emqx.service; +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ + +import cc.iotkit.common.utils.CodecUtil; +import cc.iotkit.common.utils.UniqueIdUtil; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.up.DeviceRegister; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.core.PluginInfo; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Slf4j +@Service +public class AuthVerticle extends AbstractVerticle { + + private HttpServer backendServer; + + @Setter + private int port; + + @Setter + private String serverPassword; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + + @Autowired + private PluginInfo pluginInfo; + + @Override + public void start() { + backendServer = vertx.createHttpServer(); + + //第一步 声明Router&初始化Router + Router backendRouter = Router.router(vertx); + //获取body参数,得先添加这句 + backendRouter.route().handler(BodyHandler.create()); + + //第二步 配置Router解析url + backendRouter.route(HttpMethod.POST, "/mqtt/auth").handler(rc -> { + JsonObject json = rc.getBodyAsJson(); + log.info("mqtt auth:{}", json); + try { + String clientId = json.getString("clientid"); + String username = json.getString("username"); + String password = json.getString("password"); + + //服务端插件连接 + if (clientId.equals("server") && serverPassword.equals(password)) { + httpResult(rc.response(), 200); + return; + } + + //其它客户端连接 + String[] parts = clientId.split("_"); + if (parts.length < 3) { + log.error("clientid:{}不正确", clientId); + httpResult(rc.response(), 400); + return; + } + + log.info("MQTT client auth,clientId:{},username:{},password:{}", + clientId, username, password); + + String productKey = parts[0]; + String deviceName = parts[1]; + String gwModel = parts[2]; + if (!username.equals(deviceName)) { + log.error("username:{}不正确", deviceName); + httpResult(rc.response(), 403); + return; + } + + ThingProduct product = thingService.getProduct(productKey); + if (product == null) { + log.error("获取产品信息失败,productKey:{}", productKey); + httpResult(rc.response(), 403); + return; + } + + String validPasswd = CodecUtil.md5Str(product.getProductSecret() + clientId); + if (!validPasswd.equalsIgnoreCase(password)) { + log.error("密码验证失败,期望值:{}", validPasswd); + httpResult(rc.response(), 403); + return; + } + + //网关设备注册 + ActionResult result = thingService.post( + pluginInfo.getPluginId(), + DeviceRegister.builder() + .productKey(productKey) + .deviceName(deviceName) + .model(gwModel) + .version("1.0") + .id(UniqueIdUtil.newRequestId()) + .time(System.currentTimeMillis()) + .build() + ); + if (result.getCode() != 0) { + log.error("设备注册失败:{}", result); + httpResult(rc.response(), 403); + return; + } + + Set devices = new HashSet<>(); + devices.add(productKey + "," + deviceName); + EmqxPlugin.CLIENT_DEVICE_MAP.putIfAbsent(productKey + deviceName, devices); + + httpResult(rc.response(), 200); + } catch (Throwable e) { + httpResult(rc.response(), 500); + log.error("mqtt auth failed", e); + } + }); + backendRouter.route(HttpMethod.POST, "/mqtt/acl").handler(rc -> { + String json = rc.getBodyAsString(); + log.info("mqtt acl:{}", json); + try { + //将json放到rc中,方便后续使用 + httpResult(rc.response(), 200); + } catch (Throwable e) { + httpResult(rc.response(), 500); + log.error("mqtt acl failed", e); + } + }); + + backendServer.requestHandler(backendRouter) + .listen(port, "0.0.0.0") + .onSuccess(s -> { + log.info("auth server start success,port:{}", s.actualPort()); + }).onFailure(e -> { + e.printStackTrace(); + }) + ; + } + + private void httpResult(HttpServerResponse response, int code) { + response.putHeader("Content-Type", "application/json"); + response + .setStatusCode(code); + response + .end("{\"result\": \"" + (code == 200 ? "allow" : "deny") + "\"}"); + } + + @Override + public void stop() throws Exception { + backendServer.close(voidAsyncResult -> log.info("close emqx auth server...")); + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/EmqxPlugin.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/EmqxPlugin.java new file mode 100644 index 0000000..861048b --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/EmqxPlugin.java @@ -0,0 +1,507 @@ +package cc.iotkit.plugins.emqx.service; + +import cc.iotkit.common.utils.StringUtils; +import cc.iotkit.common.utils.ThreadUtil; +import cc.iotkit.common.utils.UniqueIdUtil; +import cc.iotkit.plugin.core.IPlugin; +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.DeviceState; +import cc.iotkit.plugin.core.thing.actions.EventLevel; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.actions.up.*; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugins.emqx.conf.DeviceStatusParser; +import cc.iotkit.plugins.emqx.conf.GasAlarmDataParser; +import cc.iotkit.plugins.emqx.conf.MqttConfig; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.util.IdUtil; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.bootstrap.realize.PluginCloseListener; +import com.gitee.starblues.core.PluginCloseType; +import com.gitee.starblues.core.PluginInfo; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static cc.iotkit.plugins.emqx.conf.BCDClockGenerator.formatBCDToHex; +import static cc.iotkit.plugins.emqx.conf.BCDClockGenerator.generateBCDTime; +import static cc.iotkit.plugins.emqx.conf.CRC16ModbusUtil.buildFrame; + +/** + * @author sjg + */ +@Slf4j +@Service +public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { + + @Autowired + private PluginInfo pluginInfo; + @Autowired + private MqttConfig mqttConfig; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginConfig pluginConfig; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + + @Autowired + private AuthVerticle authVerticle; + + @Autowired + private MqttDevice mqttDevice; + + private final ScheduledThreadPoolExecutor emqxConnectTask = ThreadUtil.newScheduled(1, "emqx_connect"); + + private Vertx vertx; + private String deployedId; + + private MqttClient client; + + private boolean mqttConnected = false; + + private boolean authServerStarted = false; + + private static final Map DEVICE_ONLINE = new ConcurrentHashMap<>(); + + public static final Map> CLIENT_DEVICE_MAP = new HashMap<>(); + + @PostConstruct + public void init() { + vertx = Vertx.vertx(); + try { + //获取插件最新配置替换当前配置 + Map config = pluginConfig.getConfig(pluginInfo.getPluginId()); + BeanUtil.copyProperties(config, mqttConfig, CopyOptions.create().ignoreNullValue()); + + String serverPassword = IdUtil.fastSimpleUUID(); + MqttClientOptions options = new MqttClientOptions() + .setClientId("server") + .setUsername("server") + .setPassword(serverPassword) + .setCleanSession(true) + .setMaxInflightQueue(100) + .setMaxMessageSize(1024*1024) + .setKeepAliveInterval(60); + + if (mqttConfig.isSsl()) { + options.setSsl(true) + .setTrustAll(true); + } + client = MqttClient.create(vertx, options); + mqttDevice.setClient(client); + + authVerticle.setPort(mqttConfig.getAuthPort()); + authVerticle.setServerPassword(serverPassword); + + emqxConnectTask.scheduleWithFixedDelay(this, 3, 3, TimeUnit.SECONDS); + } catch (Throwable e) { + log.error("mqtt plugin startup error", e); + } + } + + @Override + public void run() { + if (!authServerStarted) { + try { + CountDownLatch countDownLatch = new CountDownLatch(1); + Future future = vertx.deployVerticle(authVerticle); + future.onSuccess((s -> { + deployedId = s; + countDownLatch.countDown(); + authServerStarted = true; + log.info("start emqx auth plugin success"); + })); + future.onFailure(e -> { + countDownLatch.countDown(); + authServerStarted = false; + log.error("start emqx auth plugin failed", e); + }); + countDownLatch.await(); + } catch (Exception e) { + authServerStarted = false; + log.error("start emqx auth server failed", e); + } + } + + if (mqttConnected) { + return; + } + + try { + String[] topics = mqttConfig.getTopics().split(","); + Map subscribes = new HashMap<>(topics.length); + for (String topic : topics) { + subscribes.put(topic, 1); + } + + client.connect(mqttConfig.getPort(), mqttConfig.getHost(), s -> { + if (s.succeeded()) { + log.info("client connect success."); + mqttConnected = true; + client.subscribe(subscribes, e -> { + if (e.succeeded()) { + log.info("===>subscribe success: {}", e.result()); + } else { + log.error("===>subscribe fail: ", e.cause()); + } + }); + + } else { + mqttConnected = false; + log.error("client connect fail: ", s.cause()); + } + }).publishHandler(msg -> { + String topic = msg.topicName(); + log.info("topic={}",topic); + if (topic.contains("/c/")) { + return; + } + if (topic != null && !topic.startsWith("/sys/") && topic.endsWith("/rtdvalue/report")){ + String s = Arrays.asList(topic.split("/")).get(1); + topic = "/sys/*/"+s+"/s/event/property/post"; + log.info("Client received message on [{}] payload [{}] with QoS [{}]", topic, msg.payload().toJsonObject(), msg.qosLevel()); + } + JsonObject payload = msg.payload().toJsonObject(); + + try { + //客户端连接断开 + if (topic.equals("/sys/client/disconnected")) { + offline(payload.getString("clientid")); + return; + } + + ThingDevice device = getDevice(topic); + if (device == null) { + return; + } + + //有消息上报-设备上线 + online("*", device.getDeviceName()); + + JsonObject defParams = JsonObject.mapFrom(new HashMap<>(0)); + IDeviceAction action = null; + String method = payload.getString("method", ""); + if(StringUtils.isBlank(method)){ + method = "thing.event.property.post_reply"; + } + + if (StringUtils.isBlank(method)) { + return; + } + JsonObject params = null; + + if(!StringUtils.isBlank(payload.getString("method", ""))){ + params = payload.getJsonObject("params", defParams); + }else{ + String cmd =payload.getString("cmd"); + String data =payload.getString("data"); + if(ObjectUtils.isNotEmpty(cmd)&& ObjectUtils.isNotEmpty(data)){ + if(cmd.equals("11")){ + //4.1 状态信息上报(命令码0x11) + Map map = DeviceStatusParser.parseStatusReportToMap(data); + // params= payload.getJsonObject(DeviceStatusParser.parseStatusReportToMap(data),defParams); + payload.put("params",map); + device.setDeviceName(map.get("imei").toString()); + switch(map.get("deviceType").toString()) { + case "0": + device.setProductKey("CEMpmANABN7Tt6Jh"); + + break; + case "1": + device.setProductKey("XmXYxjzihseT76As"); + + break; + case "2": + device.setProductKey("bAASX8tBjYQjBGFP"); + + break; + case "3": + device.setProductKey("WfpZZFkMxxbGfRca"); + + break; + } + + + }else if(cmd.equals("12")){ + //4.2 燃气报警器物联网模块数据上报(0x12) + payload.put("params",GasAlarmDataParser.parseHexDataToList(data)); + // GasAlarmDataParser.parseHexDataToList(data); + }else if(cmd.equals("14")){ + } + } + // params = + //解析设备数据 + } + params = payload.getJsonObject("params", defParams); + + // System.out.println(payload.getJsonObject("data", defParams)); + if (ObjectUtils.isNotEmpty(method) && "thing.lifetime.register".equalsIgnoreCase(method)) { + //子设备注册 + String subPk = params.getString("productKey"); + String subDn = params.getString("deviceName"); + String subModel = params.getString("model"); + ActionResult regResult = thingService.post( + pluginInfo.getPluginId(), + fillAction( + SubDeviceRegister.builder() + .productKey(device.getProductKey()) + .deviceName(device.getDeviceName()) + .version("1.0") + .subs(List.of( + DeviceRegister.builder() + .productKey(subPk) + .deviceName(subDn) + .model(subModel) + .build() + )) + .build() + ) + ); + if (regResult.getCode() == 0) { + //注册成功 + reply(topic, payload, 0); + Set devices = CLIENT_DEVICE_MAP.get(device.getProductKey() + device.getDeviceName()); + devices.add(subPk + "," + subDn); + } else { + //注册失败 + reply(topic, new JsonObject(), regResult.getCode()); + } + return; + } + /* - //假设为数组 + params.getMap(); + if (params.getMap().isEmpty()) { + params = new JsonObject(); + }*/ + //属性上报 + if(StringUtils.isBlank(payload.getString("method", ""))){ + + action = PropertyReport.builder() + .params(getConfigMap(payload)) + .build(); + }else{ + action = PropertyReport.builder() + .params(params.getMap()) + .build(); + } + + try { + reply(topic, payload, 0); + }catch (Exception e){ + log.error("reply error", e); + } + + if (action == null) { + return; + } + action.setId(payload.getString("id")); + action.setProductKey(device.getProductKey()); + action.setDeviceName(device.getDeviceName()); + action.setTime(System.currentTimeMillis()); + thingService.post(pluginInfo.getPluginId(), action); + + } catch (Exception e) { + log.error("message is illegal.", e); + } + }).closeHandler(e -> { + mqttConnected = false; + log.info("client closed"); + }).exceptionHandler(event -> log.error("client fail", event)); + } catch (Exception e) { + log.error("start emqx client failed", e); + } + } + + public ThingDevice getDevice(String topic) { + String[] topicParts = topic.split("/"); + if (topicParts.length < 5) { + return null; + } + return ThingDevice.builder() + .productKey(topicParts[2]) + .deviceName(topicParts[3]) + .build(); + } + + public void online(String pk, String dn) { + if (Boolean.TRUE.equals(DEVICE_ONLINE.get(dn))) { + return; + } + + //上线 + thingService.post( + pluginInfo.getPluginId(), + fillAction(DeviceStateChange.builder() + .productKey(pk) + .deviceName(dn) + .state(DeviceState.ONLINE) + .build() + ) + ); + DEVICE_ONLINE.put(dn, true); + } + + public void offline(String clientId) { + String[] parts = clientId.split("_"); + Set devices = CLIENT_DEVICE_MAP.get(parts[0] + parts[1]); + for (String device : devices) { + String[] pkDn = device.split(","); + //下线 + thingService.post( + pluginInfo.getPluginId(), + fillAction(DeviceStateChange.builder() + .productKey(pkDn[0]) + .deviceName(pkDn[1]) + .state(DeviceState.OFFLINE) + .build() + ) + ); + DEVICE_ONLINE.remove(pkDn[1]); + } + } + + private IDeviceAction fillAction(IDeviceAction action) { + action.setId(UniqueIdUtil.newRequestId()); + action.setTime(System.currentTimeMillis()); + return action; + } + + /** + * 回复设备 + */ + private void reply(String topic, JsonObject payload, int code) { + Map payloadReply = new HashMap<>(); + + // System.out.println("BCD Time: " + formatBCDToHex(generateBCDTime())); + String replyMessage = buildFrame("01","01", + "01","01","01"+formatBCDToHex(generateBCDTime())); + + if( StringUtils.isBlank(payload.getString("method"))){ + payloadReply.put("id", payload.getString("messageID")); + payloadReply.put("method", "thing.event.property.post_reply"); + payloadReply.put("code", code); +// 先校验数据类型再转换 +// 使用Vert.x原生方法避免强制转换 + JsonObject params = null; + JsonObject defParams = JsonObject.mapFrom(new HashMap<>(0)); + params = payload.getJsonObject("params", defParams); + // JsonArray attribute = payload.getJsonArray("params", new JsonArray()); // 提供默认值:ml-citation{ref="1" data="citationList"} + + JsonArray attribute = new JsonArray(); + // JsonObject jsonObject = payload.getMap().get("params").getJsonObject("params", defParams); + attribute.add(payload.getMap().get("params")); // 回退处理为单元素数组:ml-citation{ref="8" data="citationList"} + + // JsonArray attribute = payload.getJsonArray("params"); + List points = attribute.getList(); + Map attrMap = (Map) points.get(0); + List>arrList = (List>) attrMap.get("points"); + Map map = new HashMap<>(); + if(ObjectUtils.isEmpty(arrList)){ + payloadReply.put("data", attrMap); + }else{ + arrList.forEach(s->{ + map.put((String) s.get("name"),s.get("value")); + }); + payloadReply.put("data", map); + } + topic = topic.replace("/s/", "/c/") + "_reply"; + }else{ + payloadReply.put("id", payload.getString("id")); + payloadReply.put("method", payload.getString("method") + "_reply"); + payloadReply.put("code", code); + payloadReply.put("data", payload.getJsonObject("params")); + topic = topic.replace("/s/", "/c/") + "_reply"; + } + payloadReply.put("data", replyMessage); + String finalTopic = topic; + + client.publish(topic, JsonObject.mapFrom(payloadReply).toBuffer(), MqttQoS.AT_LEAST_ONCE, false, false) + .onSuccess(h -> { + log.info("publish {} success", finalTopic); + }); + /* client.publish(topic, JsonObject.mapFrom(payloadReply).toBuffer(), MqttQoS.AT_LEAST_ONCE, false, false) + .onSuccess(h -> { + log.info("publish {} success", finalTopic); + });*/ + } + + public Map getConfigMap(JsonObject payload) { + JsonArray attribute = new JsonArray(); + attribute.add(payload.getMap().get("params")); + // JsonArray attribute = payload.getJsonArray("params"); + List points = attribute.getList(); + Map attrMap = (Map) points.get(0); + List>arrList = (List>) attrMap.get("points"); + if(ObjectUtils.isEmpty(arrList)){ + return attrMap; + } + Map map = new HashMap<>(); + /* for (int i = 0; i < arrList.size(); i++) { + map.put((String) arrList.get(i).get("name"),arrList.get(i).get("value")); + }*/ + arrList.forEach(s->{ + map.put((String) s.get("name"),s.get("value")); + }); + return map; + } + + + @Override + public void close(GenericApplicationContext applicationContext, PluginInfo pluginInfo, PluginCloseType closeType) { + try { + log.info("plugin close,type:{},pluginId:{}", closeType, pluginInfo.getPluginId()); + if (deployedId != null) { + CountDownLatch wait = new CountDownLatch(1); + Future future = vertx.undeploy(deployedId); + future.onSuccess(unused -> { + log.info("emqx plugin stopped success"); + wait.countDown(); + }); + future.onFailure(h -> { + log.error("emqx plugin stopped failed", h); + wait.countDown(); + }); + wait.await(5, TimeUnit.SECONDS); + } + + client.disconnect() + .onSuccess(unused -> { + mqttConnected = false; + log.info("stop emqx connect success"); + }) + .onFailure(unused -> log.error("stop emqx connect failure")); + + emqxConnectTask.shutdown(); + + } catch (Throwable e) { + log.error("emqx plugin stop error", e); + } + } + + @Override + public Map getLinkInfo(String pk, String dn) { + return null; + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/FakeThingService.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/FakeThingService.java new file mode 100644 index 0000000..6fc1e60 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/FakeThingService.java @@ -0,0 +1,70 @@ +package cc.iotkit.plugins.emqx.service; + +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * 测试服务 + * + * @author sjg + */ +@Slf4j +public class FakeThingService implements IThingService { + + /** + * 添加测试产品 + */ + private static final Map PRODUCTS = Map.of( + "hbtgIA0SuVw9lxjB", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU", + "Rf4QSjbm65X45753", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU", + "cGCrkK7Ex4FESAwe", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU" + ); + + /** + * 添加测试设备 + */ + private static final Map DEVICES = new HashMap<>(); + + static { + for (int i = 0; i < 10; i++) { + DEVICES.put("TEST:GW:" + StringUtils.leftPad(i + "", 6, "0"), "hbtgIA0SuVw9lxjB"); + DEVICES.put("TEST_SW_" + StringUtils.leftPad(i + "", 6, "0"), "Rf4QSjbm65X45753"); + DEVICES.put("TEST_SC_" + StringUtils.leftPad(i + "", 6, "0"), "cGCrkK7Ex4FESAwe"); + } + } + + @Override + public ActionResult post(String pluginId, IDeviceAction action) { + log.info("post action:{}", action); + return ActionResult.builder().code(0).build(); + } + + @Override + public ThingProduct getProduct(String pk) { + return ThingProduct.builder() + .productKey(pk) + .productSecret(PRODUCTS.get(pk)) + .build(); + } + + @Override + public ThingDevice getDevice(String dn) { + return ThingDevice.builder() + .productKey(DEVICES.get(dn)) + .deviceName(dn) + .build(); + } + + @Override + public Map getProperty(String dn) { + return new HashMap<>(0); + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/MqttDevice.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/MqttDevice.java new file mode 100644 index 0000000..f03a3a3 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/MqttDevice.java @@ -0,0 +1,186 @@ +package cc.iotkit.plugins.emqx.service; + +import cc.iotkit.common.enums.ErrCode; +import cc.iotkit.common.exception.BizException; +import cc.iotkit.common.utils.JsonUtils; +import cc.iotkit.plugin.core.thing.IDevice; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.down.DeviceConfig; +import cc.iotkit.plugin.core.thing.actions.down.PropertyGet; +import cc.iotkit.plugin.core.thing.actions.down.PropertySet; +import cc.iotkit.plugin.core.thing.actions.down.ServiceInvoke; +import cc.iotkit.plugins.emqx.conf.IoTConfigProtocol; +import cn.hutool.core.util.ObjectUtil; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.json.JsonObject; +import io.vertx.mqtt.MqttClient; +import lombok.Setter; +import org.springframework.stereotype.Service; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cc.iotkit.plugins.emqx.conf.BCDClockGenerator.formatBCDToHex; +import static cc.iotkit.plugins.emqx.conf.BCDClockGenerator.generateBCDTime; +import static cc.iotkit.plugins.emqx.conf.CRC16ModbusUtil.buildFrame; +import static cc.iotkit.plugins.emqx.conf.IoTConfigProtocol.*; + +import static cn.hutool.core.util.NumberUtil.parseNumber; + +/** + * mqtt设备下行接口 + * + * @author sjg + */ +@Service +public class MqttDevice implements IDevice { + + @Setter + private MqttClient client; + public List processConfig(Map config) { + if (config == null || config.isEmpty()) { + throw new IllegalArgumentException("Config map cannot be null or empty"); + } + List list = new ArrayList<>(); + for (Map.Entry entry : config.entrySet()) { + Map map = new HashMap(); + String key = entry.getKey(); + String value = entry.getValue().toString(); + Integer configId = null; + try { + byte[] tlvData = null; + // int configId = getConfigId(key); + + /* if (configId == -1) { + System.err.println("Unknown config key: " + key); + continue; + } +*/ + switch(key.toLowerCase()) { + case "ip/realm": + configId = IP_REALM; + tlvData = buildTLVData(value.getBytes()); + break; + case "port": + case "keepalive": + case "reportrateupper": + + configId = (key.toLowerCase().equals("port") ? PORT : key.toLowerCase().equals("keepalive") ?KEEP_ALIVE :REPORT_RATE_UPPER); + tlvData= ByteBuffer.allocate(2).putShort( Short.parseShort(value)).array(); + // tlvData = buildShortData(parseNumber(value)); + break; + case "reportawaittime": + case "reportretrytimes": + configId = (key.toLowerCase().equals("reportawaittime") ? REPORT_AWAIT_TIME :REPORT_RETRY_TIMES); + tlvData = buildByteData(Integer.valueOf(value)); + break; + // case "username": + // case "password": + // tlvData = buildStringData(value.toString(), MAX_AUTH_LENGTH); + // break; + case "publishtopicprefix": + case "subscribetopicprefix": + configId = (key.toLowerCase().equals("publishtopicprefix") ? PUBLISH_TOPIC :SUBSCRIBE_TOPIC); + tlvData = buildStringData(value.toString(), 30); + break; + case "timesyncinternal": + configId = TIME_SYNC; + tlvData = buildBcdTimeData(value); + break; + case "card": + case "signal": + case "version": + System.out.println(key + " is read-only, skip writing"); + continue; + default: + System.out.println("Unsupported config: " + key); + continue; + } + if(ObjectUtil.isNotNull(configId)) { + String command = IoTConfigProtocol.generateWriteCommand( + 0x01, configId, tlvData); + map.put("date",command); + System.out.printf("Generated command for %s (ID:0x%04X): %s%n", + key, configId, command); + } + + } catch (Exception e) { + System.err.printf("Error processing %s: %s%n", key, e.getMessage()); + } + } + return list; + } + @Override + public ActionResult config(DeviceConfig action) { + String topic = String.format("/sys/%s/%s/c/config/set", action.getProductKey(), action.getDeviceName()); + // Map data = JsonUtils.parseObject(action.getConfig(), Map.class); + List list= processConfig(action.getConfig()); + for (int i = 0; i < list.size(); i++) { + //循环发送配置指令 + String replyMessage = buildFrame("01","01", + "01","01","01"+ list.get(i).get("data")); + send( + topic, + new JsonObject() + .put("id", action.getId()) + .put("method", "thing.config.set") + .put("params", action.getConfig()) + .put("data", replyMessage) + ); + } + + return ActionResult.builder().code(0).reason("").build(); + } + + + @Override + public ActionResult propertyGet(PropertyGet action) { + String topic = String.format("/sys/%s/%s/c/service/property/get", action.getProductKey(), action.getDeviceName()); + return send( + topic, + new JsonObject() + .put("id", action.getId()) + .put("method", "thing.service.property.get") + .put("params", action.getKeys()) + ); + } + + @Override + public ActionResult propertySet(PropertySet action) { + String topic = String.format("/sys/%s/%s/c/service/property/set", action.getProductKey(), action.getDeviceName()); + return send( + topic, + new JsonObject() + .put("id", action.getId()) + .put("method", "thing.service.property.set") + .put("params", action.getParams()) + ); + } + + @Override + public ActionResult serviceInvoke(ServiceInvoke action) { + String topic = String.format("/sys/%s/%s/c/service/%s", action.getProductKey(), action.getDeviceName(), action.getName()); + return send( + topic, + new JsonObject() + .put("id", action.getId()) + .put("method", "thing.service." + action.getName()) + .put("params", action.getParams()) + ); + } + + private ActionResult send(String topic, JsonObject payload) { + try { + client.publish(topic, payload.toBuffer(), MqttQoS.AT_LEAST_ONCE, false, false); + return ActionResult.builder().code(0).reason("").build(); + } catch (BizException e) { + return ActionResult.builder().code(e.getCode()).reason(e.getMessage()).build(); + } catch (Exception e) { + return ActionResult.builder().code(ErrCode.UNKNOWN_EXCEPTION.getKey()).reason(e.getMessage()).build(); + } + } + +} diff --git a/emqx-plugin/src/main/resources/application.yml b/emqx-plugin/src/main/resources/application.yml new file mode 100644 index 0000000..f313a37 --- /dev/null +++ b/emqx-plugin/src/main/resources/application.yml @@ -0,0 +1,9 @@ +plugin: + runMode: dev + mainPackage: cc.iotkit.plugin + +emqx: + host: 127.0.0.1 + port: 1883 + topics: /sys/# + authPort: 8104 diff --git a/emqx-plugin/src/main/resources/config.json b/emqx-plugin/src/main/resources/config.json new file mode 100644 index 0000000..564a4b6 --- /dev/null +++ b/emqx-plugin/src/main/resources/config.json @@ -0,0 +1,30 @@ +[ + { + "id": "host", + "name": "emqx ip", + "type": "text", + "value": "127.0.0.1", + "desc": "emqx ip,默认为127.0.0.1" + }, + { + "id": "port", + "name": "emqx端口", + "type": "number", + "value": 1883, + "desc": "emqx端口,默认为1883" + }, + { + "id": "auth_port", + "name": "认证端口", + "type": "number", + "value": 8104, + "desc": "emqx http认证端口,默认为8104" + }, + { + "id": "topics", + "name": "订阅主题", + "type": "text", + "value": "/sys/#", + "desc": "订阅主题多个用,隔开" + } +] \ No newline at end of file diff --git a/http-plugin/pom.xml b/http-plugin/pom.xml new file mode 100644 index 0000000..140728f --- /dev/null +++ b/http-plugin/pom.xml @@ -0,0 +1,81 @@ + + + + iot-iita-plugins + cc.iotkit.plugins + 2.0.19 + + 4.0.0 + + http-plugin + 1.0.1 + + + + + io.vertx + vertx-core + ${vertx.version} + + + + io.vertx + vertx-web + ${vertx.version} + + + + + + + dev + + true + + + dev + + + + + prod + + prod + + + + + + + + com.gitee.starblues + spring-brick-maven-packager + ${spring-brick.version} + + ${plugin.build.mode} + + http-plugin + cc.iotkit.plugins.http.Application + ${project.version} + iita + http示例插件,配置参数:端口(port)默认9081 + application.yml + + + jar + + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/http-plugin/src/main/java/cc/iotkit/plugins/http/Application.java b/http-plugin/src/main/java/cc/iotkit/plugins/http/Application.java new file mode 100644 index 0000000..2537404 --- /dev/null +++ b/http-plugin/src/main/java/cc/iotkit/plugins/http/Application.java @@ -0,0 +1,19 @@ +package cc.iotkit.plugins.http; + +import com.gitee.starblues.bootstrap.SpringPluginBootstrap; +import com.gitee.starblues.bootstrap.annotation.OneselfConfig; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * @author sjg + */ +@SpringBootApplication(scanBasePackages = "cc.iotkit.plugins.http") +@OneselfConfig(mainConfigFileName = {"application.yml"}) +@EnableConfigurationProperties +public class Application extends SpringPluginBootstrap { + + public static void main(String[] args) { + new Application().run(Application.class, args); + } +} diff --git a/http-plugin/src/main/java/cc/iotkit/plugins/http/conf/BeanConfig.java b/http-plugin/src/main/java/cc/iotkit/plugins/http/conf/BeanConfig.java new file mode 100644 index 0000000..ebc0451 --- /dev/null +++ b/http-plugin/src/main/java/cc/iotkit/plugins/http/conf/BeanConfig.java @@ -0,0 +1,29 @@ +package cc.iotkit.plugins.http.conf; + +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.LocalPluginConfig; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugins.http.service.FakeThingService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @author sjg + */ +@Component +public class BeanConfig { + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IThingService getThingService() { + return new FakeThingService(); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginConfig getPluginConfig(){ + return new LocalPluginConfig(); + } + +} diff --git a/http-plugin/src/main/java/cc/iotkit/plugins/http/conf/HttpConfig.java b/http-plugin/src/main/java/cc/iotkit/plugins/http/conf/HttpConfig.java new file mode 100644 index 0000000..f0108db --- /dev/null +++ b/http-plugin/src/main/java/cc/iotkit/plugins/http/conf/HttpConfig.java @@ -0,0 +1,28 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.plugins.http.conf; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * http配置 + * + * @author sjg + */ +@Data +@Component +@ConfigurationProperties(prefix = "http") +public class HttpConfig { + + private int port; + +} diff --git a/http-plugin/src/main/java/cc/iotkit/plugins/http/service/FakeThingService.java b/http-plugin/src/main/java/cc/iotkit/plugins/http/service/FakeThingService.java new file mode 100644 index 0000000..a11b667 --- /dev/null +++ b/http-plugin/src/main/java/cc/iotkit/plugins/http/service/FakeThingService.java @@ -0,0 +1,48 @@ +package cc.iotkit.plugins.http.service; + +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import io.vertx.core.json.JsonObject; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * 测试服务 + * + * @author sjg + */ +@Slf4j +public class FakeThingService implements IThingService { + + @Override + public ActionResult post(String pluginId, IDeviceAction action) { + log.info("post action:{}", action); + return ActionResult.builder().code(0).build(); + } + + @Override + public ThingProduct getProduct(String pk) { + return ThingProduct.builder() + .productKey("cGCrkK7Ex4FESAwe") + .productSecret("xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU") + .build(); + } + + @Override + public ThingDevice getDevice(String dn) { + return ThingDevice.builder() + .productKey("cGCrkK7Ex4FESAwe") + .deviceName(dn) + .secret("mBCr3TKstTj2KeM6") + .build(); + } + + @Override + public Map getProperty(String dn) { + return new JsonObject().put("powerstate", 1).getMap(); + } +} diff --git a/http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpDevice.java b/http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpDevice.java new file mode 100644 index 0000000..348ac70 --- /dev/null +++ b/http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpDevice.java @@ -0,0 +1,43 @@ +package cc.iotkit.plugins.http.service; + +import cc.iotkit.plugin.core.thing.IDevice; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.down.DeviceConfig; +import cc.iotkit.plugin.core.thing.actions.down.PropertyGet; +import cc.iotkit.plugin.core.thing.actions.down.PropertySet; +import cc.iotkit.plugin.core.thing.actions.down.ServiceInvoke; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * http设备下行接口 + * + * @author sjg + */ +@Service +public class HttpDevice implements IDevice { + + @Autowired + private HttpVerticle httpVerticle; + + @Override + public ActionResult config(DeviceConfig action) { + return ActionResult.builder().code(0).reason("").build(); + } + + @Override + public ActionResult propertyGet(PropertyGet action) { + throw new UnsupportedOperationException("不支持该功能"); + } + + @Override + public ActionResult propertySet(PropertySet action) { + throw new UnsupportedOperationException("不支持该功能"); + } + + @Override + public ActionResult serviceInvoke(ServiceInvoke action) { + throw new UnsupportedOperationException("不支持该功能"); + } + +} diff --git a/http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpPlugin.java b/http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpPlugin.java new file mode 100644 index 0000000..99d2ede --- /dev/null +++ b/http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpPlugin.java @@ -0,0 +1,91 @@ +package cc.iotkit.plugins.http.service; + +import cc.iotkit.common.utils.JsonUtils; +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugins.http.conf.HttpConfig; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.bootstrap.realize.PluginCloseListener; +import com.gitee.starblues.core.PluginCloseType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * @author sjg + */ +@Slf4j +@Service +public class HttpPlugin implements PluginCloseListener { + + @Autowired + private PluginInfo pluginInfo; + @Autowired + private HttpVerticle httpVerticle; + @Autowired + private HttpConfig httpConfig; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginConfig pluginConfig; + + private Vertx vertx; + private String deployedId; + + @PostConstruct + public void init() { + vertx = Vertx.vertx(); + try { + //获取插件最新配置替换当前配置 + Map config = pluginConfig.getConfig(pluginInfo.getPluginId()); + log.info("get config:{}", JsonUtils.toJsonString(config)); + BeanUtil.copyProperties(config, httpConfig, CopyOptions.create().ignoreNullValue()); + httpVerticle.setConfig(httpConfig); + + Future future = vertx.deployVerticle(httpVerticle); + future.onSuccess((s -> { + deployedId = s; + log.info("http plugin startup success"); + })); + future.onFailure((e) -> { + log.error("http plugin startup failed", e); + }); + } catch (Throwable e) { + log.error("http plugin startup error", e); + } + } + + @Override + public void close(GenericApplicationContext applicationContext, PluginInfo pluginInfo, PluginCloseType closeType) { + try { + log.info("plugin close,type:{},pluginId:{}", closeType, pluginInfo.getPluginId()); + if (deployedId != null) { + CountDownLatch wait = new CountDownLatch(1); + Future future = vertx.undeploy(deployedId); + future.onSuccess(unused -> { + log.info("http plugin stopped success"); + wait.countDown(); + }); + future.onFailure(h -> { + log.info("tcp plugin stopped failed"); + h.printStackTrace(); + wait.countDown(); + }); + wait.await(5, TimeUnit.SECONDS); + } + } catch (Throwable e) { + log.error("http plugin stop error", e); + } + } + +} diff --git a/http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpVerticle.java b/http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpVerticle.java new file mode 100644 index 0000000..257ffef --- /dev/null +++ b/http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpVerticle.java @@ -0,0 +1,206 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.plugins.http.service; + +import cc.iotkit.common.utils.StringUtils; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.DeviceState; +import cc.iotkit.plugin.core.thing.actions.EventLevel; +import cc.iotkit.plugin.core.thing.actions.up.DeviceStateChange; +import cc.iotkit.plugin.core.thing.actions.up.EventReport; +import cc.iotkit.plugin.core.thing.actions.up.PropertyReport; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugins.http.conf.HttpConfig; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * mqtt官方协议文档: + * http://iotkit-open-source.gitee.io/document/pages/device_protocol/http/#%E4%BA%8B%E4%BB%B6%E4%B8%8A%E6%8A%A5 + * + * @author sjg + */ +@Slf4j +@Component +@Data +public class HttpVerticle extends AbstractVerticle implements Handler { + private HttpConfig config; + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + @Autowired + private PluginInfo pluginInfo; + private static final Set DEVICE_ONLINE = new HashSet<>(); + private HttpServer httpServer; + + @Override + public void start() { + Executors.newSingleThreadScheduledExecutor().schedule(this::initHttpServer, 3, TimeUnit.SECONDS); + } + + private void initHttpServer() { + httpServer = vertx.createHttpServer(); + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()).handler(this); + httpServer.requestHandler(router).listen(config.getPort(), ar -> { + if (ar.succeeded()) { + log.info("http server is listening on port " + ar.result().actualPort()); + } else { + log.error("Error on starting the server", ar.cause()); + } + }); + } + + @Override + public void stop() { + httpServer.close(rst -> { + log.info("http server close:{}", rst.succeeded()); + }); + } + + @Override + public void handle(RoutingContext ctx) { + HttpServerResponse response = ctx.response(); + response.putHeader("content-type", "application/json"); + response.setStatusCode(200); + + try { + String secret = ctx.request().getHeader("secret"); + if (StringUtils.isBlank(secret)) { + log.error("secret不能为空"); + response.setStatusCode(401); + end(response); + return; + } + + HttpServerRequest request = ctx.request(); + // /sys/{productKey}/{deviceName}/properties + String path = request.path(); + String[] parts = path.split("/"); + if (parts.length < 5) { + log.error("不正确的路径"); + response.setStatusCode(500); + } + + String productKey = parts[2]; + String deviceName = parts[3]; + String type = parts[4]; + ThingDevice device = thingService.getDevice(deviceName); + if (device == null) { + log.error("认证失败,设备:{} 不存在", deviceName); + response.setStatusCode(401); + end(response); + return; + } + if (!secret.equalsIgnoreCase(device.getSecret())) { + log.error("认证失败,secret不正确,期望值:{}", device.getSecret()); + response.setStatusCode(401); + end(response); + return; + } + + //设备上线 + if (!DEVICE_ONLINE.contains(deviceName)) { + thingService.post(pluginInfo.getPluginId(), DeviceStateChange.builder() + .id(UUID.randomUUID().toString()) + .productKey(productKey) + .deviceName(deviceName) + .state(DeviceState.ONLINE) + .time(System.currentTimeMillis()) + .build()); + DEVICE_ONLINE.add(deviceName); + } + + String method = request.method().name(); + JsonObject payload = ctx.getBodyAsJson(); + + if ("event".equals(type)) { + //事件上报 + if (!"POST".equalsIgnoreCase(method)) { + response.setStatusCode(500); + log.error("请求类型不正确,期望值:POST,实际值:{}", method); + end(response); + } + thingService.post( + pluginInfo.getPluginId(), + EventReport.builder() + .id(payload.getString("id")) + .productKey(productKey) + .deviceName(deviceName) + .level(EventLevel.INFO) + .name(parts[5]) + .params(payload.getJsonObject("params").getMap()) + .time(System.currentTimeMillis()) + .build() + ); + end(response); + return; + } + + if ("properties".equals(type)) { + if ("POST".equalsIgnoreCase(method)) { + //属性上报 + thingService.post( + pluginInfo.getPluginId(), + PropertyReport.builder() + .id(UUID.randomUUID().toString()) + .productKey(productKey) + .deviceName(deviceName) + .params(payload.getJsonObject("params").getMap()) + .time(System.currentTimeMillis()) + .build() + ); + end(response); + return; + } + + if ("GET".equalsIgnoreCase(method)) { + //属性获取 + Map property = thingService.getProperty(deviceName); + response.end(new JsonObject() + .put("code", 0) + .put("data", property) + .toString()); + } + } + } catch (Exception e) { + log.error("消息处理失败", e); + response.setStatusCode(500); + end(response); + } + } + + private void end(HttpServerResponse response) { + response.end(new JsonObject() + .put("code", response.getStatusCode() == 200 ? 0 : response.getStatusCode()) + .toString()); + } + +} diff --git a/http-plugin/src/main/resources/application.yml b/http-plugin/src/main/resources/application.yml new file mode 100644 index 0000000..dc52b9e --- /dev/null +++ b/http-plugin/src/main/resources/application.yml @@ -0,0 +1,6 @@ +plugin: + runMode: prod + mainPackage: cc.iotkit.plugin + +http: + port: 9081 diff --git a/http-plugin/src/main/resources/config.json b/http-plugin/src/main/resources/config.json new file mode 100644 index 0000000..106fb5c --- /dev/null +++ b/http-plugin/src/main/resources/config.json @@ -0,0 +1,26 @@ +[ + { + "id": "port", + "name": "端口", + "type": "number", + "value": 9081, + "desc": "http端口,默认为9081" + }, + { + "id": "a", + "name": "测试参数1", + "type": "radio", + "value": 0, + "desc": "单选参数a", + "options": [ + { + "name": "值0", + "value": 0 + }, + { + "name": "值1", + "value": 11 + } + ] + } +] \ No newline at end of file diff --git a/http-plugin/src/main/resources/script.js b/http-plugin/src/main/resources/script.js new file mode 100644 index 0000000..e69de29 diff --git a/http-plugin/src/test/java/cc/iotkit/test/http/HttpTest.java b/http-plugin/src/test/java/cc/iotkit/test/http/HttpTest.java new file mode 100644 index 0000000..af7c947 --- /dev/null +++ b/http-plugin/src/test/java/cc/iotkit/test/http/HttpTest.java @@ -0,0 +1,36 @@ +package cc.iotkit.test.http; + +import cc.iotkit.common.utils.ThreadUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import io.vertx.core.json.JsonObject; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class HttpTest { + + public static void main(String[] args) { + ScheduledThreadPoolExecutor timer = ThreadUtil.newScheduled(1, "http-test"); + timer.scheduleWithFixedDelay(HttpTest::report, 0, 3, TimeUnit.SECONDS); + } + + public static void report() { + HttpResponse rst = HttpUtil.createPost("http://127.0.0.1:9084/sys/cGCrkK7Ex4FESAwe/cz00001/properties") + .header("secret", "mBCr3TKstTj2KeM6") + .body(new JsonObject() + .put("id", IdUtil.fastSimpleUUID()) + .put("params", new JsonObject() + .put("powerstate", RandomUtil.randomInt(0, 2)) + .put("rssi", RandomUtil.randomInt(-127, 127)) + .getMap() + ).encode() + ).execute(); + log.info("send result:status={},body={}", rst.getStatus(), rst.body()); + } + +} diff --git a/hydrovalve-plugin/pom.xml b/hydrovalve-plugin/pom.xml new file mode 100644 index 0000000..7573aba --- /dev/null +++ b/hydrovalve-plugin/pom.xml @@ -0,0 +1,64 @@ + + + + iot-iita-plugins + cc.iotkit.plugins + 2.0.19 + + 4.0.0 + + hydrovalve-plugin + + + + dev + + true + + + dev + + + + + prod + + prod + + + + + + + + com.gitee.starblues + spring-brick-maven-packager + ${spring-brick.version} + + ${plugin.build.mode} + + hydrovalve-plugin + cc.iotkit.plugins.hydrovalve.Application + ${project.version} + iita + modbus插件 + application.yml + + + jar + + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/Application.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/Application.java new file mode 100644 index 0000000..ed94e72 --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/Application.java @@ -0,0 +1,22 @@ +package cc.iotkit.plugins.hydrovalve; + +import com.gitee.starblues.bootstrap.SpringPluginBootstrap; +import com.gitee.starblues.bootstrap.annotation.OneselfConfig; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @Author:tfd + * @Date:2024/1/8 14:57 + */ +@SpringBootApplication(scanBasePackages = "cc.iotkit.plugins.hydrovalve") +@OneselfConfig(mainConfigFileName = {"application.yml"}) +@EnableConfigurationProperties +@EnableScheduling +public class Application extends SpringPluginBootstrap { + + public static void main(String[] args) { + new Application().run(Application.class, args); + } +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusAnalysis.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusAnalysis.java new file mode 100644 index 0000000..807f69c --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusAnalysis.java @@ -0,0 +1,23 @@ +package cc.iotkit.plugins.hydrovalve.analysis; + +/** + * @Author:tfd + * @Date:2024/1/9 15:41 + */ +public abstract class ModBusAnalysis { + /** + * 编码:将实体打包成报文 + * + * @param entity 实体 + * @return 数据报文 + */ + public abstract byte[] packCmd4Entity(ModBusEntity entity); + + /** + * 解包:将报文解码成实体 + * + * @param arrCmd 报文 + * @return 实体 + */ + public abstract ModBusEntity unPackCmd2Entity(byte[] arrCmd); +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusConstants.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusConstants.java new file mode 100644 index 0000000..ca1f24f --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusConstants.java @@ -0,0 +1,34 @@ +package cc.iotkit.plugins.hydrovalve.analysis; + +/** + * @Author:tfd + * @Date:2024/1/9 15:45 + */ +public class ModBusConstants { + public static final String MODE = "modbusMode"; + public static final String MODE_ASCII = "ASCII"; + public static final String MODE_RTU = "RTU"; + public static final String MODE_TCP = "TCP"; + + /** + * ModBus的报文结构 + */ + public static final String SN = "sn"; + public static final String ADDR = "devAddr"; + public static final String FUNC = "func"; + public static final String DATA = "data"; + + /** + * 寄存器地址和数量 + */ + public static final String REG_ADDR = "regAddr"; + public static final String REG_CNT = "regCnt"; + + public static final String REG_HOLD_STATUS = "regHoldStatus"; + + //读多个保持寄存器 + public static final byte FUN_CODE3 = 0x03; + + //写单个保持寄存器 + public static final byte FUN_CODE6 = 0x06; +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusEntity.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusEntity.java new file mode 100644 index 0000000..a5cc274 --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusEntity.java @@ -0,0 +1,42 @@ +package cc.iotkit.plugins.hydrovalve.analysis; + +import lombok.Getter; +import lombok.Setter; + +/** + * @Author:tfd + * @Date:2024/1/9 15:44 + */ +@Getter +@Setter +public class ModBusEntity { + /** + * 流水号 + */ + private int sn = 0; + + /** + * 地址 + */ + private byte devAddr = 0x01; + + /** + * 功能码 + */ + private byte func = 0x01; + + /** + * 数据域 + */ + private byte[] data = new byte[0]; + + /** + * 出错信息 + */ + private int errCode = 0; + + /** + * 出错信息 + */ + private String errMsg = ""; +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusError.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusError.java new file mode 100644 index 0000000..4beb772 --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusError.java @@ -0,0 +1,50 @@ +package cc.iotkit.plugins.hydrovalve.analysis; + +/** + * @Author:tfd + * @Date:2024/1/9 15:48 + */ +public class ModBusError { + static final String err01 = "err=01:非法的功能码"; + static final String err02 = "err=02:非法的数据地址"; + static final String err03 = "err=03:非法的数据值"; + static final String err04 = "err=04:服务器故障"; + static final String err05 = "err=05:确认。"; + static final String err06 = "err=06:服务器繁忙"; + static final String err10 = "err=10:网关故障:网关路经是无效的"; + static final String err11 = "err=11:网关故障:目标设备没有响应"; + + /** + * 获取出错信息 + * @param code 出错代码 + * @return 出错信息 + */ + static String getError(int code) { + if (code == 1) { + return err01; + } + if (code == 2) { + return err02; + } + if (code == 3) { + return err03; + } + if (code == 4) { + return err04; + } + if (code == 5) { + return err05; + } + if (code == 6) { + return err06; + } + if (code == 10) { + return err10; + } + if (code == 11) { + return err11; + } + + return ""; + } +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusRtuAnalysis.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusRtuAnalysis.java new file mode 100644 index 0000000..c9626ba --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusRtuAnalysis.java @@ -0,0 +1,171 @@ +package cc.iotkit.plugins.hydrovalve.analysis; + +import cc.iotkit.common.utils.StringUtils; +import cc.iotkit.plugins.hydrovalve.utils.ByteUtils; +import lombok.extern.slf4j.Slf4j; + +/** + * @Author:tfd + * @Date:2024/1/9 15:53 + */ +@Slf4j +public class ModBusRtuAnalysis extends ModBusAnalysis { + /** + * 校验CRC16 + * + * @param arrCmd + * @return + */ + public static int getCRC16(byte[] arrCmd) { + int iSize = arrCmd.length - 2; + + // 检查:帧长度 + if (iSize < 2) { + return 0; + } + + int wCrcMathematics = 0xA001; + + int usCrc16 = 0x00; + + //16位的CRC寄存器 + int byteCrc16Lo = 0xFF; + int byteCrc16Hi = 0xFF; + //临时变量 + int byteSaveHi = 0x00; + int byteSaveLo = 0x00; + + //CRC多项式码的寄存器 + int byteCl = wCrcMathematics % 0x100; + int byteCh = wCrcMathematics / 0x100; + + for (int i = 0; i < iSize; i++) { + byteCrc16Lo &= 0xFF; + byteCrc16Hi &= 0xFF; + byteSaveHi &= 0xFF; + byteSaveLo &= 0xFF; + + byteCrc16Lo ^= arrCmd[i]; //每一个数据与CRC寄存器进行异或 + for (int k = 0; k < 8; k++) { + byteCrc16Lo &= 0xFF; + byteCrc16Hi &= 0xFF; + + byteSaveHi = byteCrc16Hi; + byteSaveLo = byteCrc16Lo; + byteCrc16Hi /= 2; //高位右移一位 + byteCrc16Lo /= 2; //低位右移一位 + if ((byteSaveHi & 0x01) == 0x01) //如果高位字节最后一位为1 + { + byteCrc16Lo |= 0x80; //则低位字节右移后前面补1 + } //否则自动补0 + if ((byteSaveLo & 0x01) == 0x01) //如果高位字节最后一位为1,则与多项式码进行异或 + { + byteCrc16Hi ^= byteCh; + byteCrc16Lo ^= byteCl; + } + } + } + + + usCrc16 = (byteCrc16Hi & 0xff) * 0x100 + (byteCrc16Lo & 0xff); + + return usCrc16; + } + + /** + * 解码 + * + * @param arrCmd 报文 + * @return 是否成功 + */ + @Override + public ModBusEntity unPackCmd2Entity(byte[] arrCmd) { + ModBusEntity entity = new ModBusEntity(); + + int iSize = arrCmd.length; + if (iSize < 4) { + return null; + } + + // 地址码 + byte byAddr = arrCmd[0]; + entity.setDevAddr(byAddr); + + // 功能码 + byte byFun = arrCmd[1]; + entity.setFunc(byFun); + + // 数据域 + int iDataSize = iSize - 4; + entity.setData(new byte[iDataSize]); + byte[] arrData = entity.getData(); + System.arraycopy(arrCmd, 2, arrData, 0, iDataSize); + + // 校验CRC + int wCrc16OK = getCRC16(arrCmd); + byte crcH = (byte) (wCrc16OK & 0xff); + byte crcL = (byte) ((wCrc16OK & 0xff00) >> 8); + if (arrCmd[arrCmd.length - 1] == crcL && arrCmd[arrCmd.length - 2] == crcH) { + return entity; + } + + return null; + } + + /** + * 编码 + * + * @return 编码是否成功 + */ + @Override + public byte[] packCmd4Entity(ModBusEntity entity) { + int iSize = entity.getData().length; + + byte[] arrCmd = new byte[iSize + 4]; + + // 地址码 + arrCmd[0] = entity.getDevAddr(); + + // 功能码 + arrCmd[1] = entity.getFunc(); + + // 数据域 + System.arraycopy(entity.getData(), 0, arrCmd, 2, iSize); + + // 校验CRC + int wCrc16 = getCRC16(arrCmd); + arrCmd[arrCmd.length - 2] = (byte) (wCrc16 % 0x100); + arrCmd[arrCmd.length - 1] = (byte) (wCrc16 / 0x100); + + return arrCmd; + } + + public static void main(String[] args) { +// String hexString = "0103020457FB7A"; + String hexString = "01060001000119CA"; + ModBusRtuAnalysis a=new ModBusRtuAnalysis(); + ModBusEntity b=a.unPackCmd2Entity(ByteUtils.hexStrToBinaryStr(hexString)); + int lenth=b.getData()[0]; + byte[] val = new byte[lenth]; + System.arraycopy(b.getData(), 1, val, 0, lenth); + log.info("ret:"+Integer.parseInt(ByteUtils.BinaryToHexString(val,false), 16)); + ModBusEntity c=new ModBusEntity(); + c.setDevAddr((byte) 1); + c.setFunc(ModBusConstants.FUN_CODE3); + Integer dz=0; + Integer dzsl=10; + String a1=StringUtils.leftPad(dz.toHexString(dz),4,'0')+StringUtils.leftPad(dz.toHexString(dzsl),4,'0'); + c.setData(ByteUtils.hexStrToBinaryStr(a1)); + byte[] d=a.packCmd4Entity(c); + log.info("ret1:"+ByteUtils.BinaryToHexString(d,false)); + ModBusEntity e=new ModBusEntity(); + e.setDevAddr((byte) 1); + e.setFunc(ModBusConstants.FUN_CODE6); + Integer dze=0; + Integer dzsle=1; + String a1e=StringUtils.leftPad(dz.toHexString(dze),4,'0')+StringUtils.leftPad(dz.toHexString(dzsle),4,'0'); + e.setData(ByteUtils.hexStrToBinaryStr(a1e)); + byte[] f=a.packCmd4Entity(e); + log.info("rete:"+ByteUtils.BinaryToHexString(f,false)); + } +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/conf/BeanConfig.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/conf/BeanConfig.java new file mode 100644 index 0000000..3bcea2a --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/conf/BeanConfig.java @@ -0,0 +1,37 @@ +package cc.iotkit.plugins.hydrovalve.conf; + +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.IPluginScript; +import cc.iotkit.plugin.core.LocalPluginConfig; +import cc.iotkit.plugin.core.LocalPluginScript; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugins.hydrovalve.service.FakeThingService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @Author:tfd + * @Date:2024/1/8 14:58 + */ +@Component +public class BeanConfig { + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IThingService getThingService() { + return new FakeThingService(); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginScript getPluginScript() { + return new LocalPluginScript("script.js"); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginConfig getPluginConfig(){ + return new LocalPluginConfig(); + } +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/conf/HydrovalveConfig.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/conf/HydrovalveConfig.java new file mode 100644 index 0000000..0625e32 --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/conf/HydrovalveConfig.java @@ -0,0 +1,21 @@ +package cc.iotkit.plugins.hydrovalve.conf; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author:tfd + * @Date:2024/1/8 15:04 + */ +@Data +@Component +@ConfigurationProperties(prefix = "hydrovalve") +public class HydrovalveConfig { + + private String host; + + private int port; + + private int interval; +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/FakeThingService.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/FakeThingService.java new file mode 100644 index 0000000..ef429fa --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/FakeThingService.java @@ -0,0 +1,46 @@ +package cc.iotkit.plugins.hydrovalve.service; + +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author:tfd + * @Date:2024/1/8 14:58 + */ +@Slf4j +public class FakeThingService implements IThingService { + + @Override + public ActionResult post(String pluginId, IDeviceAction action) { + log.info("post action:{}", action); + return ActionResult.builder().code(0).build(); + } + + @Override + public ThingProduct getProduct(String pk) { + return ThingProduct.builder() + .productKey("cGCrkK7Ex4FESAwe") + .productSecret("aaaaaaaa") + .build(); + } + + @Override + public ThingDevice getDevice(String dn) { + return ThingDevice.builder() + .productKey("cGCrkK7Ex4FESAwe") + .deviceName(dn) + .build(); + } + + @Override + public Map getProperty(String dn) { + return new HashMap<>(0); + } +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModBusDevice.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModBusDevice.java new file mode 100644 index 0000000..a9ad8b9 --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModBusDevice.java @@ -0,0 +1,62 @@ +package cc.iotkit.plugins.hydrovalve.service; + +import cc.iotkit.common.utils.StringUtils; +import cc.iotkit.plugin.core.thing.IDevice; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.down.DeviceConfig; +import cc.iotkit.plugin.core.thing.actions.down.PropertyGet; +import cc.iotkit.plugin.core.thing.actions.down.PropertySet; +import cc.iotkit.plugin.core.thing.actions.down.ServiceInvoke; +import cc.iotkit.plugins.hydrovalve.analysis.ModBusConstants; +import cc.iotkit.plugins.hydrovalve.analysis.ModBusEntity; +import cc.iotkit.plugins.hydrovalve.analysis.ModBusRtuAnalysis; +import cc.iotkit.plugins.hydrovalve.utils.ByteUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * @Author:tfd + * @Date:2024/1/10 11:06 + */ +@Service +public class ModBusDevice implements IDevice { + + @Autowired + private ModbusVerticle modbusVerticle; + + ModBusRtuAnalysis analysis=new ModBusRtuAnalysis(); + + @Override + public ActionResult config(DeviceConfig action) { + return ActionResult.builder().code(0).reason("").build(); + } + + @Override + public ActionResult propertyGet(PropertyGet action) { + return null; + } + + @Override + public ActionResult propertySet(PropertySet action) { + ModBusEntity read=new ModBusEntity(); + String devAddr=action.getDeviceName().split("_")[1]; + read.setFunc(ModBusConstants.FUN_CODE6); + read.setDevAddr(Byte.parseByte(devAddr)); + Integer addr=0; + for (Map.Entry entry : action.getParams().entrySet()) { + int val = Integer.parseInt((String) entry.getValue()); + String a1= StringUtils.leftPad(addr.toHexString(addr),4,'0')+StringUtils.leftPad(addr.toHexString(val),4,'0'); + read.setData(ByteUtils.hexStrToBinaryStr(a1)); + byte[] msg = analysis.packCmd4Entity(read); + modbusVerticle.sendMsg(msg); + } + return ActionResult.builder().code(0).reason("success").build(); + } + + @Override + public ActionResult serviceInvoke(ServiceInvoke action) { + return null; + } +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModbusPlugin.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModbusPlugin.java new file mode 100644 index 0000000..d313f60 --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModbusPlugin.java @@ -0,0 +1,99 @@ +package cc.iotkit.plugins.hydrovalve.service; + +import cc.iotkit.common.utils.JsonUtils; +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.IPluginScript; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugins.hydrovalve.conf.HydrovalveConfig; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.bootstrap.realize.PluginCloseListener; +import com.gitee.starblues.core.PluginCloseType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * @Author:tfd + * @Date:2024/1/8 14:57 + */ +@Slf4j +@Service +public class ModbusPlugin implements PluginCloseListener { + + @Autowired + private PluginInfo pluginInfo; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginScript pluginScript; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginConfig pluginConfig; + + @Autowired + private HydrovalveConfig modbusConfig; + + @Autowired + private ModbusVerticle modbusVerticle; + + private Vertx vertx; + private String deployedId; + + @PostConstruct + public void init(){ + vertx = Vertx.vertx(); + try { + //获取插件最新配置替换当前配置 + Map config = pluginConfig.getConfig(pluginInfo.getPluginId()); + BeanUtil.copyProperties(config, modbusConfig, CopyOptions.create().ignoreNullValue()); + modbusVerticle.setModbusConfig(modbusConfig); + + Future future = vertx.deployVerticle(modbusVerticle); + future.onSuccess((s -> { + deployedId = s; + log.info("modbus plugin started success,config:"+ JsonUtils.toJsonString(modbusConfig)); + })); + future.onFailure(Throwable::printStackTrace); + } catch (Throwable e) { + log.error("modbus plugin error", e); + } + } + + @SneakyThrows + @Override + public void close(GenericApplicationContext applicationContext, PluginInfo pluginInfo, PluginCloseType closeType) { + log.info("plugin close,type:{},pluginId:{}", closeType, pluginInfo.getPluginId()); + if (deployedId != null) { + CountDownLatch wait = new CountDownLatch(1); + Future future = vertx.undeploy(deployedId); + future.onSuccess(unused -> { + log.info("modbus plugin stopped success"); + wait.countDown(); + }); + future.onFailure(h -> { + log.info("modbus plugin stopped failed"); + h.printStackTrace(); + wait.countDown(); + }); + wait.await(5, TimeUnit.SECONDS); + } + } + +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModbusVerticle.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModbusVerticle.java new file mode 100644 index 0000000..09f7ca6 --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModbusVerticle.java @@ -0,0 +1,182 @@ +package cc.iotkit.plugins.hydrovalve.service; + +import cc.iotkit.common.utils.StringUtils; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.DeviceState; +import cc.iotkit.plugin.core.thing.actions.up.DeviceRegister; +import cc.iotkit.plugin.core.thing.actions.up.DeviceStateChange; +import cc.iotkit.plugin.core.thing.actions.up.PropertyReport; +import cc.iotkit.plugins.hydrovalve.analysis.ModBusConstants; +import cc.iotkit.plugins.hydrovalve.analysis.ModBusEntity; +import cc.iotkit.plugins.hydrovalve.analysis.ModBusRtuAnalysis; +import cc.iotkit.plugins.hydrovalve.conf.HydrovalveConfig; +import cc.iotkit.plugins.hydrovalve.utils.ByteUtils; +import cn.hutool.core.util.IdUtil; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * @Author:tfd + * @Date:2024/1/9 17:06 + */ +@Slf4j +@Service +public class ModbusVerticle extends AbstractVerticle { + @Getter + @Setter + private HydrovalveConfig modbusConfig; + + private NetClient netClient; + + private NetSocket socket; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + + ModBusRtuAnalysis analysis=new ModBusRtuAnalysis(); + + @Autowired + private PluginInfo pluginInfo; + + private int connectState = 0; + + private long timerID; + + @Override + public void start() { + log.info("init start"); + } + + @Scheduled(initialDelay = 2, fixedRate = 5, timeUnit = TimeUnit.SECONDS) + public void initClient() { + if (connectState > 0) { + return; + } + connectState = 1; + NetClientOptions options = new NetClientOptions(); + options.setReconnectAttempts(Integer.MAX_VALUE); + options.setReconnectInterval(20000L); + netClient = vertx.createNetClient(options); + netClient.connect(modbusConfig.getPort(), modbusConfig.getHost()) + .onComplete(result -> { + if (result.succeeded()) { + connectState = 2; + log.info("connect modbus slave success"); + socket = result.result(); + thingService.post(pluginInfo.getPluginId(), DeviceRegister.builder() + .id(UUID.randomUUID().toString()) + .productKey("PYWH4r8xBzsfn3XB") + .deviceName(String.format("modbus_%d", 1)) + .build()); + stateChange(DeviceState.ONLINE,String.format("modbus_%d", 1)); + socket.handler(data -> { + String hexStr = ByteUtils.BinaryToHexString(data.getBytes(), false); + log.info("modbus received message:{}", hexStr); + //获取功能码 + if(0x03==data.getBytes()[1]){ + ModBusEntity ret = analysis.unPackCmd2Entity(data.getBytes()); + Map params = new HashMap<>(); + params.put("devSwith" , Integer.parseInt(ByteUtils.BinaryToHexString(getData(ret.getData()),false)));//数据标识 + thingService.post(pluginInfo.getPluginId(), + PropertyReport.builder().deviceName(String.format("modbus_%d", ret.getDevAddr())).productKey("PwMfpXmp4ZWkGahn") + .params(params) + .build() + ); + } + }).closeHandler(res -> { + connectState = 0; + vertx.cancelTimer(timerID); + log.info("modbus tcp connection closed!"); + stateChange(DeviceState.OFFLINE,String.format("modbus_%d", 1)); + } + ).exceptionHandler(res -> { + connectState = 0; + vertx.cancelTimer(timerID); + log.info("modbus tcp connection exce!"); + stateChange(DeviceState.OFFLINE,String.format("modbus_%d", 1)); + }); + timerID = vertx.setPeriodic(modbusConfig.getInterval(), t -> { + readDataTask(); + }); + } else { + connectState = 0; + log.info("connect modbus tcp error", result.cause()); + } + }) + .onFailure(e -> { + log.error("modbus connect failed", e); + connectState = 0; + }) + ; + } + + private void readDataTask() { + log.info("readData:" + socket); + if (socket != null) { + ModBusEntity read=new ModBusEntity(); + read.setFunc(ModBusConstants.FUN_CODE3); + read.setDevAddr((byte) 1); + Integer addr=1; + Integer length=1; + String a1= StringUtils.leftPad(addr.toHexString(addr),4,'0')+StringUtils.leftPad(addr.toHexString(length),4,'0'); + read.setData(ByteUtils.hexStrToBinaryStr(a1)); + byte[] msg = analysis.packCmd4Entity(read); + sendMsg(msg); + } + } + + @Override + public void stop() throws Exception { + if (netClient != null) { + netClient.close(); + } + vertx.cancelTimer(timerID); + connectState = 0; + super.stop(); + } + + private byte[] getData(byte[] data) { + int lenth=data[0]; + byte[] val = new byte[lenth]; + System.arraycopy(data, 1, val, 0, lenth); + return val; + } + + private void stateChange(DeviceState state,String deviceName) { + thingService.post(pluginInfo.getPluginId(), + DeviceStateChange.builder() + .id(IdUtil.simpleUUID()) + .state(state).productKey("PYWH4r8xBzsfn3XB").deviceName(deviceName) + .time(System.currentTimeMillis()) + .build()); + } + + public void sendMsg(byte[] msg) { + log.info("modbus send msg data:{}", ByteUtils.BinaryToHexString(msg,false)); + Buffer data = Buffer.buffer(msg); + socket.write(data, r -> { + if (r.succeeded()) { + log.info("modbus msg send success:{}", ByteUtils.BinaryToHexString(msg,false)); + } else { + log.error("modbus msg send failed", r.cause()); + } + }); + } +} diff --git a/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/utils/ByteUtils.java b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/utils/ByteUtils.java new file mode 100644 index 0000000..f66a7e1 --- /dev/null +++ b/hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/utils/ByteUtils.java @@ -0,0 +1,58 @@ +package cc.iotkit.plugins.hydrovalve.utils; + +/** + * @Author:tfd + * @Date:2024/1/8 15:15 + */ +public class ByteUtils { + /** + * 将十六进制的字符串转换成字节数组 + * + * @param hexString + * @return + */ + public static byte[] hexStrToBinaryStr(String hexString) { + + if (hexString==null) { + return null; + } + try { + hexString = hexString.replaceAll(" ", ""); + int len = hexString.length(); + int index = 0; + byte[] bytes = new byte[len / 2]; + while (index < len) { + String sub = hexString.substring(index, index + 2); + bytes[index/2] = (byte)Integer.parseInt(sub,16); + index += 2; + } + return bytes; + }catch (Exception e){ + return null; + } + + } + + /** + * 将字节数组转换成十六进制的字符串 + * + * @return + */ + public static String BinaryToHexString(byte[] bytes,boolean isBalank) { + String hexStr = "0123456789ABCDEF"; + String result = ""; + String hex = ""; + Boolean feStart=true; + for (byte b : bytes) { + hex = String.valueOf(hexStr.charAt((b & 0xF0) >> 4)); + hex += String.valueOf(hexStr.charAt(b & 0x0F)); + if("FE".equals(hex) && feStart){ + continue; + }else { + feStart=false; + } + result += hex + (isBalank?" ":""); + } + return result; + } +} diff --git a/hydrovalve-plugin/src/main/resources/application.yml b/hydrovalve-plugin/src/main/resources/application.yml new file mode 100644 index 0000000..3eecb34 --- /dev/null +++ b/hydrovalve-plugin/src/main/resources/application.yml @@ -0,0 +1,8 @@ +plugin: + runMode: prod + mainPackage: cc.iotkit.plugin + +hydrovalve: + host: xxxx + port: 38807 + interval: 20000 diff --git a/hydrovalve-plugin/src/main/resources/config.json b/hydrovalve-plugin/src/main/resources/config.json new file mode 100644 index 0000000..22abf0c --- /dev/null +++ b/hydrovalve-plugin/src/main/resources/config.json @@ -0,0 +1,23 @@ +[ + { + "id": "host", + "name": "服务端ip", + "type": "text", + "value": "25on621889.goho.co", + "desc": "服务端ip" + }, + { + "id": "port", + "name": "服务端端口", + "type": "number", + "value": 38807, + "desc": "服务端端口" + }, + { + "id": "interval", + "name": "采集频率", + "type": "number", + "value": 20000, + "desc": "采集频率" + } +] \ No newline at end of file diff --git a/modbus-plugin/pom.xml b/modbus-plugin/pom.xml new file mode 100644 index 0000000..8c0b724 --- /dev/null +++ b/modbus-plugin/pom.xml @@ -0,0 +1,74 @@ + + + + iot-iita-plugins + cc.iotkit.plugins + 2.0.19 + + 4.0.0 + + modbus-plugin + + + + + com.digitalpetri.modbus + modbus-master-tcp + 1.2.0 + + + + + + + dev + + true + + + dev + + + + + prod + + prod + + + + + + + + com.gitee.starblues + spring-brick-maven-packager + ${spring-brick.version} + + ${plugin.build.mode} + + modbus-plugin + cc.iotkit.plugins.modbus.Application + ${project.version} + iita + modbus示例插件 + application.yml + + + jar + + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/Application.java b/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/Application.java new file mode 100644 index 0000000..c6c0676 --- /dev/null +++ b/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/Application.java @@ -0,0 +1,21 @@ +package cc.iotkit.plugins.modbus; + +import com.gitee.starblues.bootstrap.SpringPluginBootstrap; +import com.gitee.starblues.bootstrap.annotation.OneselfConfig; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @author sjg + */ +@SpringBootApplication(scanBasePackages = "cc.iotkit.plugins.modbus") +@OneselfConfig(mainConfigFileName = {"application.yml"}) +@EnableConfigurationProperties +@EnableScheduling +public class Application extends SpringPluginBootstrap { + + public static void main(String[] args) { + new Application().run(Application.class, args); + } +} diff --git a/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/conf/BeanConfig.java b/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/conf/BeanConfig.java new file mode 100644 index 0000000..11fa414 --- /dev/null +++ b/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/conf/BeanConfig.java @@ -0,0 +1,36 @@ +package cc.iotkit.plugins.modbus.conf; + +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.IPluginScript; +import cc.iotkit.plugin.core.LocalPluginConfig; +import cc.iotkit.plugin.core.LocalPluginScript; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugins.modbus.service.FakeThingService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @author sjg + */ +@Component +public class BeanConfig { + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IThingService getThingService() { + return new FakeThingService(); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginScript getPluginScript() { + return new LocalPluginScript("script.js"); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginConfig getPluginConfig(){ + return new LocalPluginConfig(); + } +} diff --git a/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/service/FakeThingService.java b/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/service/FakeThingService.java new file mode 100644 index 0000000..03ef55a --- /dev/null +++ b/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/service/FakeThingService.java @@ -0,0 +1,47 @@ +package cc.iotkit.plugins.modbus.service; + +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * 测试服务 + * + * @author sjg + */ +@Slf4j +public class FakeThingService implements IThingService { + + @Override + public ActionResult post(String pluginId, IDeviceAction action) { + log.info("post action:{}", action); + return ActionResult.builder().code(0).build(); + } + + @Override + public ThingProduct getProduct(String pk) { + return ThingProduct.builder() + .productKey("cGCrkK7Ex4FESAwe") + .productSecret("aaaaaaaa") + .build(); + } + + @Override + public ThingDevice getDevice(String dn) { + return ThingDevice.builder() + .productKey("cGCrkK7Ex4FESAwe") + .deviceName(dn) + .build(); + } + + @Override + public Map getProperty(String dn) { + return new HashMap<>(0); + } +} diff --git a/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/service/ModbusPlugin.java b/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/service/ModbusPlugin.java new file mode 100644 index 0000000..3f4fed9 --- /dev/null +++ b/modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/service/ModbusPlugin.java @@ -0,0 +1,132 @@ +package cc.iotkit.plugins.modbus.service; + +import cc.iotkit.plugin.core.IPluginScript; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.DeviceState; +import cc.iotkit.plugin.core.thing.actions.up.DeviceRegister; +import cc.iotkit.plugin.core.thing.actions.up.DeviceStateChange; +import cc.iotkit.plugin.core.thing.actions.up.PropertyReport; +import cc.iotkit.script.IScriptEngine; +import com.digitalpetri.modbus.master.ModbusTcpMaster; +import com.digitalpetri.modbus.master.ModbusTcpMasterConfig; +import com.digitalpetri.modbus.requests.ReadHoldingRegistersRequest; +import com.digitalpetri.modbus.responses.ReadHoldingRegistersResponse; +import com.fasterxml.jackson.core.type.TypeReference; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.bootstrap.realize.PluginCloseListener; +import com.gitee.starblues.core.PluginCloseType; +import com.gitee.starblues.core.PluginInfo; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.ReferenceCountUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +/** + * @author sjg + */ +@Slf4j +@Service +public class ModbusPlugin implements PluginCloseListener { + + @Autowired + private PluginInfo pluginInfo; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginScript pluginScript; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + + private IScriptEngine scriptEngine; + + private final ModbusTcpMasterConfig config = new ModbusTcpMasterConfig.Builder("localhost").setPort(502).build(); + private ModbusTcpMaster master; + + private final Set registeredDevice = new HashSet<>(); + + private final int[] slaves = new int[]{1, 2, 3}; + + private final Map DATA_CACHE = new HashMap<>(); + + @PostConstruct + public void init() { + master = new ModbusTcpMaster(config); + CompletableFuture connect = master.connect(); + connect.thenAccept(modbusTcpMaster -> System.out.println("111:" + modbusTcpMaster.getConfig())); + + //获取脚本引擎 + scriptEngine = pluginScript.getScriptEngine(pluginInfo.getPluginId()); + } + + @Scheduled(initialDelay = 3000, fixedDelay = 1000) + public void taskRead() { + for (int slave : slaves) { + CompletableFuture future = + master.sendRequest(new ReadHoldingRegistersRequest(0, 10), slave); + + future.thenAccept(response -> { + String rspBytes = ByteBufUtil.hexDump(response.getRegisters()); + ReferenceCountUtil.release(response); + + log.info("receive Response: " + rspBytes); + //相同数据不处理 + if (DATA_CACHE.getOrDefault(slave, "").equals(rspBytes)) { + return; + } + DATA_CACHE.put(slave, rspBytes); + + if (!registeredDevice.contains(slave)) { + //第一次读取自动注册设备 + thingService.post(pluginInfo.getPluginId(), DeviceRegister.builder() + .id(UUID.randomUUID().toString()) + .productKey("cGCrkK7Ex4FESAwe") + .deviceName(String.format("modbus_%d", slave)) + .build()); + registeredDevice.add(slave); + //并上线 + thingService.post(pluginInfo.getPluginId(), DeviceStateChange.builder() + .id(UUID.randomUUID().toString()) + .productKey("cGCrkK7Ex4FESAwe") + .deviceName(String.format("modbus_%d", slave)) + .state(DeviceState.ONLINE) + .build()); + } + + //调用脚本解码 + Map rst = scriptEngine.invokeMethod(new TypeReference<>() { + }, "decode", rspBytes); + if (rst == null || rst.isEmpty()) { + return; + } + + //属性上报 + thingService.post(pluginInfo.getPluginId(), PropertyReport.builder() + .id(UUID.randomUUID().toString()) + .productKey("cGCrkK7Ex4FESAwe") + .deviceName(String.format("modbus_%d", slave)) + .params(rst) + .build()); + }); + } + } + + @Override + public void close(GenericApplicationContext applicationContext, PluginInfo pluginInfo, PluginCloseType closeType) { + try { + log.info("plugin close,type:{},pluginId:{}", closeType, pluginInfo.getPluginId()); + master.disconnect(); + } catch (Throwable e) { + log.error("modbus plugin stop error", e); + } + } + +} diff --git a/modbus-plugin/src/main/resources/application.yml b/modbus-plugin/src/main/resources/application.yml new file mode 100644 index 0000000..2401036 --- /dev/null +++ b/modbus-plugin/src/main/resources/application.yml @@ -0,0 +1,5 @@ +plugin: + runMode: prod + mainPackage: cc.iotkit.plugin + + diff --git a/modbus-plugin/src/main/resources/script.js b/modbus-plugin/src/main/resources/script.js new file mode 100644 index 0000000..41f2c62 --- /dev/null +++ b/modbus-plugin/src/main/resources/script.js @@ -0,0 +1,24 @@ +function hexToByte(hexString) { + if (hexString.length % 2 !== 0) { + throw new Error('Invalid hex string. String must have an even number of characters.'); + } + + let byteArray = []; + for (let i = 0; i < hexString.length; i += 4) { + byteArray.push(parseInt(hexString.substr(i, 4), 16)); + } + + return byteArray; +} + +this.decode=function(hex){ + try{ + const bytes=hexToByte(hex); + return { + "rssi":bytes[0], + "powerstate":bytes[1] + }; + }catch(e){ + return {}; + } +} \ No newline at end of file diff --git a/mqtt-plugin/pom.xml b/mqtt-plugin/pom.xml new file mode 100644 index 0000000..d77e8d8 --- /dev/null +++ b/mqtt-plugin/pom.xml @@ -0,0 +1,85 @@ + + + + iot-iita-plugins + cc.iotkit.plugins + 2.10.19 + + 4.0.0 + + mqtt-plugin + + + + + io.vertx + vertx-core + ${vertx.version} + + + + io.vertx + vertx-mqtt + ${vertx.version} + + + + io.netty + netty-codec-mqtt + + + + + + + dev + + true + + + dev + + + + + prod + + prod + + + + + + + + com.gitee.starblues + spring-brick-maven-packager + ${spring-brick.version} + + ${plugin.build.mode} + + mqtt-plugin + cc.iotkit.plugins.mqtt.Application + ${project.version} + iita + mqtt示例插件 + application.yml + + + jar + + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/Application.java b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/Application.java new file mode 100644 index 0000000..1b5cf53 --- /dev/null +++ b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/Application.java @@ -0,0 +1,19 @@ +package cc.iotkit.plugins.mqtt; + +import com.gitee.starblues.bootstrap.SpringPluginBootstrap; +import com.gitee.starblues.bootstrap.annotation.OneselfConfig; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * @author sjg + */ +@SpringBootApplication(scanBasePackages = "cc.iotkit.plugins.mqtt") +@OneselfConfig(mainConfigFileName = {"application.yml"}) +@EnableConfigurationProperties +public class Application extends SpringPluginBootstrap { + + public static void main(String[] args) { + new Application().run(Application.class, args); + } +} diff --git a/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/conf/BeanConfig.java b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/conf/BeanConfig.java new file mode 100644 index 0000000..c72f817 --- /dev/null +++ b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/conf/BeanConfig.java @@ -0,0 +1,28 @@ +package cc.iotkit.plugins.mqtt.conf; + +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.LocalPluginConfig; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugins.mqtt.service.FakeThingService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @author sjg + */ +@Component +public class BeanConfig { + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IThingService getThingService() { + return new FakeThingService(); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginConfig getPluginConfig(){ + return new LocalPluginConfig(); + } +} diff --git a/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/conf/MqttConfig.java b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/conf/MqttConfig.java new file mode 100644 index 0000000..660dcc9 --- /dev/null +++ b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/conf/MqttConfig.java @@ -0,0 +1,31 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.plugins.mqtt.conf; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "mqtt") +public class MqttConfig { + + private int port; + + private String sslKey; + + private String sslCert; + + private boolean ssl; + + private boolean useWebSocket; + +} diff --git a/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/FakeThingService.java b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/FakeThingService.java new file mode 100644 index 0000000..a82fab9 --- /dev/null +++ b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/FakeThingService.java @@ -0,0 +1,70 @@ +package cc.iotkit.plugins.mqtt.service; + +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * 测试服务 + * + * @author sjg + */ +@Slf4j +public class FakeThingService implements IThingService { + + /** + * 添加测试产品 + */ + private static final Map PRODUCTS = Map.of( + "hbtgIA0SuVw9lxjB", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU", + "Rf4QSjbm65X45753", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU", + "cGCrkK7Ex4FESAwe", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU" + ); + + /** + * 添加测试设备 + */ + private static final Map DEVICES = new HashMap<>(); + + static { + for (int i = 0; i < 10; i++) { + DEVICES.put("TEST:GW:" + StringUtils.leftPad(i + "", 6, "0"), "hbtgIA0SuVw9lxjB"); + DEVICES.put("TEST_SW_" + StringUtils.leftPad(i + "", 6, "0"), "Rf4QSjbm65X45753"); + DEVICES.put("TEST_SC_" + StringUtils.leftPad(i + "", 6, "0"), "cGCrkK7Ex4FESAwe"); + } + } + + @Override + public ActionResult post(String pluginId, IDeviceAction action) { + log.info("post action:{}", action); + return ActionResult.builder().code(0).build(); + } + + @Override + public ThingProduct getProduct(String pk) { + return ThingProduct.builder() + .productKey(pk) + .productSecret(PRODUCTS.get(pk)) + .build(); + } + + @Override + public ThingDevice getDevice(String dn) { + return ThingDevice.builder() + .productKey(DEVICES.get(dn)) + .deviceName(dn) + .build(); + } + + @Override + public Map getProperty(String dn) { + return new HashMap<>(0); + } +} diff --git a/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttDevice.java b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttDevice.java new file mode 100644 index 0000000..ea718bf --- /dev/null +++ b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttDevice.java @@ -0,0 +1,88 @@ +package cc.iotkit.plugins.mqtt.service; + +import cc.iotkit.common.enums.ErrCode; +import cc.iotkit.common.exception.BizException; +import cc.iotkit.plugin.core.thing.IDevice; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.down.DeviceConfig; +import cc.iotkit.plugin.core.thing.actions.down.PropertyGet; +import cc.iotkit.plugin.core.thing.actions.down.PropertySet; +import cc.iotkit.plugin.core.thing.actions.down.ServiceInvoke; +import io.vertx.core.json.JsonObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * mqtt设备下行接口 + * + * @author sjg + */ +@Service +public class MqttDevice implements IDevice { + + @Autowired + private MqttVerticle mqttVerticle; + + @Override + public ActionResult config(DeviceConfig action) { + return ActionResult.builder().code(0).reason("").build(); + } + + @Override + public ActionResult propertyGet(PropertyGet action) { + String topic = String.format("/sys/%s/%s/c/service/property/get", action.getProductKey(), action.getDeviceName()); + return send( + topic, + action.getDeviceName(), + new JsonObject() + .put("id", action.getId()) + .put("method", "thing.service.property.get") + .put("params", action.getKeys()) + .toString() + ); + } + + @Override + public ActionResult propertySet(PropertySet action) { + String topic = String.format("/sys/%s/%s/c/service/property/set", action.getProductKey(), action.getDeviceName()); + return send( + topic, + action.getDeviceName(), + new JsonObject() + .put("id", action.getId()) + .put("method", "thing.service.property.set") + .put("params", action.getParams()) + .toString() + ); + } + + @Override + public ActionResult serviceInvoke(ServiceInvoke action) { + String topic = String.format("/sys/%s/%s/c/service/%s", action.getProductKey(), action.getDeviceName(), action.getName()); + return send( + topic, + action.getDeviceName(), + new JsonObject() + .put("id", action.getId()) + .put("method", "thing.service." + action.getName()) + .put("params", action.getParams()) + .toString() + ); + } + + private ActionResult send(String topic, String deviceName, String payload) { + try { + mqttVerticle.publish( + deviceName, + topic, + payload + ); + return ActionResult.builder().code(0).reason("").build(); + } catch (BizException e) { + return ActionResult.builder().code(e.getCode()).reason(e.getMessage()).build(); + } catch (Exception e) { + return ActionResult.builder().code(ErrCode.UNKNOWN_EXCEPTION.getKey()).reason(e.getMessage()).build(); + } + } + +} diff --git a/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttPlugin.java b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttPlugin.java new file mode 100644 index 0000000..306b4bf --- /dev/null +++ b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttPlugin.java @@ -0,0 +1,94 @@ +package cc.iotkit.plugins.mqtt.service; + +import cc.iotkit.plugin.core.IPlugin; +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugins.mqtt.conf.MqttConfig; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.bootstrap.realize.PluginCloseListener; +import com.gitee.starblues.core.PluginCloseType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * @author sjg + */ +@Slf4j +@Service +public class MqttPlugin implements PluginCloseListener, IPlugin { + + @Autowired + private PluginInfo pluginInfo; + @Autowired + private MqttVerticle mqttVerticle; + @Autowired + private MqttConfig mqttConfig; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginConfig pluginConfig; + + private Vertx vertx; + private String deployedId; + + @PostConstruct + public void init() { + vertx = Vertx.vertx(); + try { + //获取插件最新配置替换当前配置 + Map config = pluginConfig.getConfig(pluginInfo.getPluginId()); + BeanUtil.copyProperties(config, mqttConfig, CopyOptions.create().ignoreNullValue()); + mqttVerticle.setConfig(mqttConfig); + + Future future = vertx.deployVerticle(mqttVerticle); + future.onSuccess((s -> { + deployedId = s; + log.info("mqtt plugin started success"); + })); + future.onFailure((e) -> { + log.error("mqtt plugin startup failed", e); + }); + } catch (Throwable e) { + log.error("mqtt plugin startup error", e); + } + } + + @Override + public void close(GenericApplicationContext applicationContext, PluginInfo pluginInfo, PluginCloseType closeType) { + try { + log.info("plugin close,type:{},pluginId:{}", closeType, pluginInfo.getPluginId()); + if (deployedId != null) { + CountDownLatch wait = new CountDownLatch(1); + Future future = vertx.undeploy(deployedId); + future.onSuccess(unused -> { + log.info("mqtt plugin stopped success"); + wait.countDown(); + }); + future.onFailure(h -> { + log.info("tcp plugin stopped failed"); + h.printStackTrace(); + wait.countDown(); + }); + wait.await(5, TimeUnit.SECONDS); + } + } catch (Throwable e) { + log.error("mqtt plugin stop error", e); + } + } + + @Override + public Map getLinkInfo(String pk, String dn) { + return null; + } +} diff --git a/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttVerticle.java b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttVerticle.java new file mode 100644 index 0000000..6feefc1 --- /dev/null +++ b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttVerticle.java @@ -0,0 +1,443 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.plugins.mqtt.service; + +import cc.iotkit.common.enums.ErrCode; +import cc.iotkit.common.exception.BizException; +import cc.iotkit.common.utils.CodecUtil; +import cc.iotkit.common.utils.StringUtils; +import cc.iotkit.common.utils.UniqueIdUtil; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.DeviceState; +import cc.iotkit.plugin.core.thing.actions.EventLevel; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.actions.up.*; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import cc.iotkit.plugins.mqtt.conf.MqttConfig; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.core.PluginInfo; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import io.netty.handler.codec.mqtt.MqttProperties; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.mqtt.*; +import io.vertx.mqtt.messages.codes.MqttSubAckReasonCode; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * mqtt官方协议文档: + * http://iotkit-open-source.gitee.io/document/pages/device_protocol/mqtt/#%E7%BD%91%E5%85%B3%E8%BF%9E%E6%8E%A5%E5%92%8C%E6%B3%A8%E5%86%8C + * + * @author sjg + */ +@Slf4j +@Component +@Data +public class MqttVerticle extends AbstractVerticle implements Handler { + + private MqttServer mqttServer; + private final Map endpointMap = new HashMap<>(); + /** + * 增加一个客户端连接clientid-连接状态池,避免mqtt关闭的时候走异常断开和mqtt断开的handler,导致多次离线消息 + */ + private static final Map MQTT_CONNECT_POOL = new ConcurrentHashMap<>(); + private static final Map DEVICE_ONLINE = new ConcurrentHashMap<>(); + + private MqttConfig config; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + + @Autowired + private PluginInfo pluginInfo; + + @Override + public void start() { + Executors.newSingleThreadScheduledExecutor().schedule(this::initMqttServer, 3, TimeUnit.SECONDS); + } + + private void initMqttServer() { + MqttServerOptions options = new MqttServerOptions() + .setPort(config.getPort()); + if (config.isSsl()) { + options = options.setSsl(true) + .setKeyCertOptions(new PemKeyCertOptions() + .setKeyPath(config.getSslKey()) + .setCertPath(config.getSslCert())); + } + options.setUseWebSocket(config.isUseWebSocket()); + options.setTcpKeepAlive(true); + + mqttServer = MqttServer.create(vertx, options); + mqttServer.endpointHandler(this).listen(ar -> { + if (ar.succeeded()) { + log.info("MQTT server is listening on port " + ar.result().actualPort()); + } else { + log.error("Error on starting the server", ar.cause()); + } + }); + } + + @Override + public void handle(MqttEndpoint endpoint) { + log.info("MQTT client:{} request to connect, clean session = {}", endpoint.clientIdentifier(), endpoint.isCleanSession()); + + MqttAuth auth = endpoint.auth(); + if (auth == null) { + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED); + return; + } + //mqtt连接认证信息: + /* + * mqttClientId: productKey_deviceName_model + * mqttUserName: deviceName + * mqttPassword: md5(产品密钥,mqttClientId) + */ + String clientId = endpoint.clientIdentifier(); + String[] parts = clientId.split("_"); + if (parts.length < 3) { + log.error("clientId:{}不正确", clientId); + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_CLIENT_IDENTIFIER_NOT_VALID); + return; + } + + log.info("MQTT client auth,clientId:{},username:{},password:{}", + clientId, auth.getUsername(), auth.getPassword()); + + String productKey = parts[0]; + String deviceName = parts[1]; + String gwModel = parts[2]; + if (!auth.getUsername().equals(deviceName)) { + log.error("username:{}不正确", deviceName); + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD); + return; + } + + ThingProduct product = thingService.getProduct(productKey); + if (product == null) { + log.error("获取产品信息失败,productKey:{}", productKey); + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD); + return; + } + + String validPasswd = CodecUtil.md5Str(product.getProductSecret() + clientId); + if (!validPasswd.equalsIgnoreCase(auth.getPassword())) { + log.error("密码验证失败,期望值:{}", validPasswd); + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD); + return; + } + + //网关设备注册 + ActionResult result = thingService.post( + pluginInfo.getPluginId(), + fillAction( + DeviceRegister.builder() + .productKey(productKey) + .deviceName(deviceName) + .model(gwModel) + .version("1.0") + .build() + ) + ); + if (result.getCode() != 0) { + log.error("设备注册失败:{}", result); + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED); + return; + } + + //保存设备与连接关系 + endpointMap.put(deviceName, endpoint); + MQTT_CONNECT_POOL.put(clientId, true); + + log.info("MQTT client keep alive timeout = {} ", endpoint.keepAliveTimeSeconds()); + + endpoint.accept(false); + + endpoint.closeHandler((v) -> { + // 网络不好时也会出发,但是设备仍然可以发消息 + log.warn("client connection closed,clientId:{}", clientId); + if (Boolean.FALSE.equals(MQTT_CONNECT_POOL.get(clientId))) { + MQTT_CONNECT_POOL.remove(clientId); + return; + } + //下线 + offline(productKey, deviceName); + DEVICE_ONLINE.clear(); + //删除设备与连接关系 + endpointMap.remove(deviceName); + }).disconnectMessageHandler(disconnectMessage -> { + log.info("Received disconnect from client, reason code = {}", disconnectMessage.code()); + if (!MQTT_CONNECT_POOL.get(clientId)) { + return; + } + //下线 + offline(productKey, deviceName); + //删除设备与连接关系 + endpointMap.remove(deviceName); + MQTT_CONNECT_POOL.put(clientId, false); + DEVICE_ONLINE.clear(); + }).subscribeHandler(subscribe -> { + List reasonCodes = new ArrayList<>(); + for (MqttTopicSubscription s : subscribe.topicSubscriptions()) { + log.info("Subscription for {},with QoS {}", s.topicName(), s.qualityOfService()); + try { + String topic = s.topicName(); + ThingDevice device = getDevice(topic); + //添加设备对应连接 + endpointMap.put(device.getDeviceName(), endpoint); + online(device.getProductKey(), device.getDeviceName()); + reasonCodes.add(MqttSubAckReasonCode.qosGranted(s.qualityOfService())); + } catch (Throwable e) { + log.error("subscribe failed,topic:" + s.topicName(), e); + reasonCodes.add(MqttSubAckReasonCode.NOT_AUTHORIZED); + } + } + // ack the subscriptions request + endpoint.subscribeAcknowledge(subscribe.messageId(), reasonCodes, MqttProperties.NO_PROPERTIES); + }).unsubscribeHandler(unsubscribe -> { + for (String topic : unsubscribe.topics()) { + ThingDevice device = getDevice(topic); + //删除设备对应连接 + endpointMap.remove(device.getDeviceName()); + //下线 + offline(device.getProductKey(), device.getDeviceName()); + DEVICE_ONLINE.remove(device.getDeviceName()); + } + + // ack the subscriptions request + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); + }).publishHandler(message -> { + String topic = message.topicName(); + JsonObject payload = message.payload().toJsonObject(); + log.info("Received message:topic={},payload={}, with QoS {}", topic, payload, + message.qosLevel()); + + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { + endpoint.publishAcknowledge(message.messageId()); + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { + endpoint.publishReceived(message.messageId()); + } + if (payload.isEmpty()) { + return; + } + + ThingDevice device = getDevice(topic); + if (device == null) { + return; + } + + //有消息上报-设备上线 + online(device.getProductKey(), device.getDeviceName()); + + if (!MQTT_CONNECT_POOL.get(clientId)) { + //保存设备与连接关系 + endpointMap.put(deviceName, endpoint); + MQTT_CONNECT_POOL.put(clientId, true); + log.info("mqtt client reconnect success,clientId:{}", clientId); + } + + try { + JsonObject defParams = JsonObject.mapFrom(new HashMap<>(0)); + IDeviceAction action = null; + + String method = payload.getString("method", ""); + if (StringUtils.isBlank(method)) { + return; + } + JsonObject params = payload.getJsonObject("params", defParams); + + if ("thing.lifetime.register".equalsIgnoreCase(method)) { + //子设备注册 + String subPk = params.getString("productKey"); + String subDn = params.getString("deviceName"); + ActionResult regResult = thingService.post( + pluginInfo.getPluginId(), + fillAction( + SubDeviceRegister.builder() + .productKey(productKey) + .deviceName(deviceName) + .model(gwModel) + .version("1.0") + .subs(List.of( + DeviceRegister.builder() + .productKey(subPk) + .deviceName(subDn) + .model(params.getString("model")) + .version("1.0") + .build() + )) + .build() + ) + ); + if (regResult.getCode() == 0) { + //注册成功 + reply(endpoint, topic, payload); + } else { + //注册失败 + reply(endpoint, topic, new JsonObject(), regResult.getCode()); + } + return; + } + + if ("thing.event.property.post".equalsIgnoreCase(method)) { + //属性上报 + action = PropertyReport.builder() + .params(params.getMap()) + .build(); + reply(endpoint, topic, payload); + } else if (method.startsWith("thing.event.")) { + //事件上报 + action = EventReport.builder() + .name(method.replace("thing.event.", "")) + .level(EventLevel.INFO) + .params(params.getMap()) + .build(); + reply(endpoint, topic, payload); + } else if (method.startsWith("thing.service.") && method.endsWith("_reply")) { + //服务回复 + action = ServiceReply.builder() + .name(method.replaceAll("thing\\.service\\.(.*)_reply", "$1")) + .code(payload.getInteger("code", 0)) + .params(params.getMap()) + .build(); + } + + if (action == null) { + return; + } + action.setId(payload.getString("id")); + action.setProductKey(device.getProductKey()); + action.setDeviceName(device.getDeviceName()); + action.setTime(System.currentTimeMillis()); + thingService.post(pluginInfo.getPluginId(), action); + } catch (Throwable e) { + log.error("handler message failed,topic:" + message.topicName(), e); + } + }).publishReleaseHandler(endpoint::publishComplete); + + } + + public void online(String pk, String dn) { + if (Boolean.TRUE.equals(DEVICE_ONLINE.get(dn))) { + return; + } + + //上线 + thingService.post( + pluginInfo.getPluginId(), + fillAction(DeviceStateChange.builder() + .productKey(pk) + .deviceName(dn) + .state(DeviceState.ONLINE) + .build() + ) + ); + DEVICE_ONLINE.put(dn, true); + } + + /** + * 回复设备 + */ + private void reply(MqttEndpoint endpoint, String topic, JsonObject payload) { + reply(endpoint, topic, payload, 0); + } + + /** + * 回复设备 + */ + private void reply(MqttEndpoint endpoint, String topic, JsonObject payload, int code) { + Map payloadReply = new HashMap<>(); + payloadReply.put("id", payload.getString("id")); + payloadReply.put("method", payload.getString("method") + "_reply"); + payloadReply.put("code", code); + payloadReply.put("data", payload.getJsonObject("params")); + + endpoint.publish(topic + "_reply", JsonObject.mapFrom(payloadReply).toBuffer(), MqttQoS.AT_LEAST_ONCE, false, false); + } + + private IDeviceAction fillAction(IDeviceAction action) { + action.setId(UniqueIdUtil.newRequestId()); + action.setTime(System.currentTimeMillis()); + return action; + } + + @Override + public void stop() { + for (MqttEndpoint endpoint : endpointMap.values()) { + String clientId = endpoint.clientIdentifier(); + String[] parts = clientId.split("_"); + if (parts.length < 3) { + continue; + } + + //下线 + offline(parts[0], parts[1]); + DEVICE_ONLINE.clear(); + } + if(mqttServer!=null) { + mqttServer.close(); + } + } + + private void offline(String productKey, String deviceName) { + thingService.post( + pluginInfo.getPluginId(), + fillAction( + DeviceStateChange.builder() + .productKey(productKey) + .deviceName(deviceName) + .state(DeviceState.OFFLINE) + .build() + ) + ); + } + + public void publish(String deviceName, String topic, String msg) { + MqttEndpoint endpoint = endpointMap.get(deviceName); + if (endpoint == null) { + throw new BizException(ErrCode.SEND_DESTINATION_NOT_FOUND); + } + Future result = endpoint.publish(topic, Buffer.buffer(msg), + MqttQoS.AT_LEAST_ONCE, false, false); + result.onFailure(e -> log.error("public topic failed", e)); + result.onSuccess(integer -> log.info("publish success,topic:{},payload:{}", topic, msg)); + } + + public ThingDevice getDevice(String topic) { + String[] topicParts = topic.split("/"); + if (topicParts.length < 5) { + return null; + } + return ThingDevice.builder() + .productKey(topicParts[2]) + .deviceName(topicParts[3]) + .build(); + } + +} diff --git a/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MyChannelInitializer.java b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MyChannelInitializer.java new file mode 100644 index 0000000..6ed126d --- /dev/null +++ b/mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MyChannelInitializer.java @@ -0,0 +1,14 @@ +package cc.iotkit.plugins.mqtt.service; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.mqtt.MqttDecoder; + +public class MyChannelInitializer extends ChannelInitializer { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + System.out.println("initChannel设置最大帧大小为"); + ch.pipeline().addLast("decoder", new MqttDecoder(10485760)); // c10MB + // 其他处理器... + } +} diff --git a/mqtt-plugin/src/main/resources/application.yml b/mqtt-plugin/src/main/resources/application.yml new file mode 100644 index 0000000..778daac --- /dev/null +++ b/mqtt-plugin/src/main/resources/application.yml @@ -0,0 +1,6 @@ +plugin: + runMode: prod + mainPackage: cc.iotkit.plugin + +mqtt: + port: 1883 diff --git a/mqtt-plugin/src/main/resources/config.json b/mqtt-plugin/src/main/resources/config.json new file mode 100644 index 0000000..a029bb3 --- /dev/null +++ b/mqtt-plugin/src/main/resources/config.json @@ -0,0 +1,9 @@ +[ + { + "id": "port", + "name": "端口", + "type": "number", + "value": 1883, + "desc": "mqtt端口,默认为1883" + } +] \ No newline at end of file diff --git a/mqtt-test-client/pom.xml b/mqtt-test-client/pom.xml new file mode 100644 index 0000000..9d6ede3 --- /dev/null +++ b/mqtt-test-client/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + cc.iotkit.plugins + mqtt-test-client + 1.0.0 + jar + + + 11 + 11 + UTF-8 + + + + + + io.vertx + vertx-core + 4.2.2 + + + + io.vertx + vertx-mqtt + 4.2.2 + + + + org.projectlombok + lombok + 1.18.26 + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + commons-codec + commons-codec + 1.15 + + + + com.fasterxml.jackson.core + jackson-core + 2.14.2 + + + + com.fasterxml.jackson.core + jackson-databind + 2.14.2 + + + + org.springframework.boot + spring-boot-starter + 2.7.13 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.13 + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/Application.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/Application.java new file mode 100644 index 0000000..cbfe194 --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/Application.java @@ -0,0 +1,10 @@ +package cc.iotkit.test.mqtt; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +@SpringBootApplication(scanBasePackages = {"cc.iotkit"}) +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/config/BeanConfig.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/config/BeanConfig.java new file mode 100644 index 0000000..2646ba2 --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/config/BeanConfig.java @@ -0,0 +1,24 @@ +package cc.iotkit.test.mqtt.config; + + +import cc.iotkit.test.mqtt.performance.ConnectionTest; +import cc.iotkit.test.mqtt.performance.ReportTest; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +@Component +public class BeanConfig { + + @Bean + @ConditionalOnProperty(name = "case", havingValue = "ConnectionTest") + ConnectionTest getConnectionTest() { + return new ConnectionTest(); + } + + @Bean + @ConditionalOnProperty(name = "case", havingValue = "ReportTest") + ReportTest getReportTest() { + return new ReportTest(); + } +} diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/config/MqttConfig.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/config/MqttConfig.java new file mode 100644 index 0000000..cf1dbff --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/config/MqttConfig.java @@ -0,0 +1,26 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.config; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "mqtt") +public class MqttConfig { + + private String host; + + private int port = 1883; + +} diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/model/Request.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/model/Request.java new file mode 100644 index 0000000..91371ed --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/model/Request.java @@ -0,0 +1,26 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Request { + + private String id; + + private String method; + + private Object params; +} \ No newline at end of file diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/model/Response.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/model/Response.java new file mode 100644 index 0000000..d1b3268 --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/model/Response.java @@ -0,0 +1,31 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.model; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Response { + + private String id; + + private int code; + + private String method; + + private Map data; +} \ No newline at end of file diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/performance/ConnectionTest.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/performance/ConnectionTest.java new file mode 100644 index 0000000..7f9c4f0 --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/performance/ConnectionTest.java @@ -0,0 +1,69 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.performance; + +import cc.iotkit.test.mqtt.config.MqttConfig; +import cc.iotkit.test.mqtt.service.Gateway; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import javax.annotation.PostConstruct; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * 连接压力测试 + */ +@Slf4j +public class ConnectionTest { + + @Autowired + private MqttConfig mqttConfig; + + @Value("${start:0}") + private int start; + + @Value("${end:10}") + private int end; + + @SneakyThrows + @PostConstruct + public void init() { + + ExecutorService executor = Executors.newCachedThreadPool(); + for (int i = start; i < end; i++) { + int finalI = i; + executor.submit(() -> { + log.info("start gateway " + (finalI + 1)); + Gateway gateway = new Gateway(mqttConfig, "hbtgIA0SuVw9lxjB", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU", + "TEST:GW:T" + StringUtils.leftPad(finalI + "", 6, "0")); + +// gateway.addSubDevice("Rf4QSjbm65X45753", +// "TEST_SW_" + StringUtils.leftPad(finalI + "", 6, "0"), +// "S01"); +// +// gateway.addSubDevice("cGCrkK7Ex4FESAwe", +// "TEST_SC_" + StringUtils.leftPad(finalI + "", 6, "0"), +// "S01"); +// +// gateway.addSubDevice("xpsYHExTKPFaQMS7", +// "TEST_LT_" + StringUtils.leftPad(finalI + "", 6, "0"), +// "L01"); + + gateway.start(); + }); + } + + System.in.read(); + } +} diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/performance/ReportTest.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/performance/ReportTest.java new file mode 100644 index 0000000..b953c10 --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/performance/ReportTest.java @@ -0,0 +1,88 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.performance; + +import cc.iotkit.test.mqtt.config.MqttConfig; +import cc.iotkit.test.mqtt.model.Request; +import cc.iotkit.test.mqtt.service.Gateway; +import cc.iotkit.test.mqtt.service.ReportTask; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * 上报压力测试 + */ +@Slf4j +public class ReportTest { + + @Autowired + private MqttConfig mqttConfig; + + @Value("${start:0}") + private int start; + + @Value("${end:10}") + private int end; + + @PostConstruct + public void init() { + ExecutorService executor = Executors.newCachedThreadPool(); + for (int i = start; i < end; i++) { + int finalI = i; + executor.submit(() -> { + log.info("start gateway " + (finalI + 1)); + Gateway gateway = new Gateway(mqttConfig, "hbtgIA0SuVw9lxjB", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU", + "TEST:GW:" + StringUtils.leftPad(finalI + "", 6, "0")); + + gateway.addSubDevice("Rf4QSjbm65X45753", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU", + "TEST_SW_" + StringUtils.leftPad(finalI + "", 6, "0"), + "S01"); + + gateway.addSubDevice("cGCrkK7Ex4FESAwe", "xdkKUymrEGSCYWswqCvSPyRSFvH5j7CU", + "TEST_SC_" + StringUtils.leftPad(finalI + "", 6, "0"), + "S01"); + + gateway.onDeviceOnline((device) -> { + String pk = device.getProductKey(); + if (!"Rf4QSjbm65X45753".equals(pk)) { + return; + } + + //设备上线后添32加上报定时任务 + ReportTask reportTask = new ReportTask(gateway.getClient()); + reportTask.addTask(String.format("/sys/%s/%s/s/event/property/post", + pk, device.getDeviceName()), + () -> { + Request request = new Request(); + request.setId(UUID.randomUUID().toString()); + request.setMethod("thing.event.property.post"); + Map param = new HashMap<>(); + param.put("volt", Math.round(Math.random() * 100)); + request.setParams(param); + return request; + }); + reportTask.start(10); + }); + + gateway.start(); + }); + } + + } +} diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Device.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Device.java new file mode 100644 index 0000000..d37f076 --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Device.java @@ -0,0 +1,28 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.service; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Device { + + protected String productKey; + + private String productSecret ; + + protected String deviceName; + + private String model; +} diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Gateway.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Gateway.java new file mode 100644 index 0000000..b627ac0 --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Gateway.java @@ -0,0 +1,164 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.service; + + +import cc.iotkit.test.mqtt.config.MqttConfig; +import cc.iotkit.test.mqtt.model.Request; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.Json; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import io.vertx.mqtt.messages.MqttConnAckMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Data +public class Gateway extends Device { + + private List subDevices = new ArrayList<>(); + + private Consumer deviceOnlineListener; + + private MqttClient client; + + private boolean isConnecting; + + private boolean registered; + + private MqttConfig mqttConfig; + + public Gateway(MqttConfig mqttConfig,String productKey, String productSecret, String deviceName) { + super(productKey, productSecret, deviceName, "GW01"); + this.mqttConfig=mqttConfig; + } + + @SneakyThrows + public void start() { + ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); + executorService.scheduleAtFixedRate(this::connect, 0, 3, TimeUnit.SECONDS); + } + + private void connect() { + if (client != null && client.isConnected()) { + return; + } + + if (isConnecting) { + return; + } + + String clientId = String.format("%s_%s_%s", productKey, deviceName, getModel()); + + try { + isConnecting = true; + MqttClientOptions options = new MqttClientOptions(); + options.setUsername(this.deviceName); + options.setPassword(DigestUtils.md5Hex(getProductSecret() + clientId)); + options.setCleanSession(true); + options.setKeepAliveInterval(30); + options.setClientId(clientId); + options.setReconnectInterval(3000); + options.setReconnectAttempts(100); + + client = MqttClient.create(Vertxs.getVertx(), options); + + CountDownLatch countDownLatch = new CountDownLatch(1); + client.connect(mqttConfig.getPort(), mqttConfig.getHost(), s -> { + if (s.succeeded()) { + log.info("mqtt connected,clientId:{}", clientId); + countDownLatch.countDown(); + registered = true; + } else { + log.info("mqtt connect failed,clientId:{}", clientId); + countDownLatch.countDown(); + } + }); + countDownLatch.await(); + if(!registered){ + return; + } + + // 订阅 + String topic = String.format("/sys/%s/%s/c/#", productKey, deviceName); + log.info("subscribe topic:{}", topic); + + client.subscribe(topic, 1, r -> { + //配置获取 +// String configGetTopic = String.format("/sys/%s/%s/s/config/get", productKey, deviceName); +// Request configRequest = new Request(); +// configRequest.setId(UUID.randomUUID().toString()); +// String configPayload = JsonUtils.toJsonString(configRequest); +// client.publish(configGetTopic, Buffer.buffer(configPayload), MqttQoS.AT_LEAST_ONCE, false, false); +// log.info("publish message,topic:{},payload:{}", configGetTopic, configPayload); + + //注册子设备 + for (Device subDevice : subDevices) { + log.info("start register sub device,pk:{},dn:{}", subDevice.getProductKey(), subDevice.getDeviceName()); + Request request = new Request(); + request.setId(UUID.randomUUID().toString()); + request.setParams(subDevice); + request.setMethod("thing.lifetime.register"); + String registerTopic = String.format("/sys/%s/%s/s/register", productKey, deviceName); + String payload = Json.encode(request); + client.publish(registerTopic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false); + log.info("publish message,topic:{},payload:{}", registerTopic, payload); + } + }); + + client.publishHandler(new MessageHandler(client, this, deviceOnlineListener)); + + client.closeHandler((v) -> { + log.info("{} closed,reconnecting...", deviceName); + client.disconnect(); + }); + + } catch (Throwable e) { + log.error("connect mqtt-broker error", e); + } finally { + isConnecting = false; + } + } + + public void addSubDevice(String productKey, String productSecret, String deviceName, String model) { + subDevices.add(new Device(productKey, productSecret, deviceName, model)); + } + + public void onDeviceOnline(Consumer listener) { + this.deviceOnlineListener = listener; + } + + + public static class OnConnected implements Handler> { + + @Override + public void handle(AsyncResult mqttConnAckMessageAsyncResult) { + + } + } + +} diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/MessageHandler.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/MessageHandler.java new file mode 100644 index 0000000..b033429 --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/MessageHandler.java @@ -0,0 +1,100 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.service; + + +import cc.iotkit.test.mqtt.model.Request; +import cc.iotkit.test.mqtt.model.Response; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.Json; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.messages.MqttPublishMessage; +import lombok.Data; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +@Slf4j +@Data +public class MessageHandler implements Handler { + + private MqttClient client; + private Gateway gateway; + private Consumer deviceOnlineListener; + + public MessageHandler(MqttClient client, Gateway gateway, Consumer deviceOnlineListener) { + this.client = client; + this.gateway = gateway; + this.deviceOnlineListener = deviceOnlineListener; + } + + @SneakyThrows + + @Override + public void handle(MqttPublishMessage msg) { + try { + String topic = msg.topicName(); + String payload = msg.payload().toString(); + + log.info("received msg,topic:{},payload:{}", topic, payload); + + if (topic.endsWith("register_reply")) { + Response response = Json.decodeValue(payload, Response.class); + //子设备注册成功 + if (response.getCode() == 0) { + Map data = response.getData(); + String productKey = data.get("productKey").toString(); + String deviceName = data.get("deviceName").toString(); + if (StringUtils.isBlank(productKey)) { + deviceOnlineListener.accept(new Device(productKey, "", deviceName, "")); + return; + } + + //订阅子设备消息 + String subTopic = String.format("/sys/%s/%s/c/#", productKey, deviceName); + log.info("subscribe topic:{}", subTopic); + client.subscribe(subTopic, 1, r -> { + if (deviceOnlineListener != null) { + deviceOnlineListener.accept(new Device(productKey, "", deviceName, "")); + } + }); + } + } + + if (topic.endsWith("_reply")) { + return; + } + Request request = Json.decodeValue(payload, Request.class); + + Response response = new Response(request.getId(), 0, request.getMethod(), new HashMap<>()); + client.publish(topic.replace("/c/", "/s/") + "_reply", + Buffer.buffer(Json.encode(response)), MqttQoS.AT_LEAST_ONCE, false, false); + + //属性设置后上报属性 + String setTopic = "/c/service/property/set"; + if (topic.endsWith(setTopic)) { + request.setId(UUID.randomUUID().toString()); + client.publish(topic.replace(setTopic, "/s/event/property/post"), + Buffer.buffer(Json.encode(request)), MqttQoS.AT_LEAST_ONCE, false, false); + } + } catch (Throwable e) { + log.info("receive msg error", e); + } + } + + +} diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/ReportTask.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/ReportTask.java new file mode 100644 index 0000000..063d50b --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/ReportTask.java @@ -0,0 +1,67 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.service; + +import cc.iotkit.test.mqtt.model.Request; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.Json; +import io.vertx.mqtt.MqttClient; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class ReportTask { + + private final MqttClient client; + private final Map> taskMap = new HashMap<>(); + private ScheduledExecutorService taskService = null; + + public ReportTask(MqttClient client) { + this.client = client; + } + + public void start(int interval) { + if (taskService == null) { + taskService = Executors.newScheduledThreadPool(1); + taskService.scheduleWithFixedDelay(this::send, 3, interval, TimeUnit.SECONDS); + } + } + + private void send() { + taskMap.forEach((topic, action) -> { + try { + Request request = action.call(); + if (request == null) { + return; + } + if (!client.isConnected()) { + return; + } + String msg = Json.encode(request); + log.info("send msg,topic:{},payload:{}", topic, msg); + client.publish(topic, Buffer.buffer(msg), MqttQoS.AT_LEAST_ONCE, false, false); + + } catch (Throwable e) { + log.error("send error", e); + } + }); + } + + public void addTask(String topic, Callable callable) { + taskMap.put(topic, callable); + } +} diff --git a/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Vertxs.java b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Vertxs.java new file mode 100644 index 0000000..99a1389 --- /dev/null +++ b/mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Vertxs.java @@ -0,0 +1,21 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.test.mqtt.service; + +import io.vertx.core.Vertx; + +public class Vertxs { + + private static final Vertx INSTANCE = Vertx.vertx(); + + public static Vertx getVertx() { + return INSTANCE; + } +} diff --git a/mqtt-test-client/src/main/resources/application.yml b/mqtt-test-client/src/main/resources/application.yml new file mode 100644 index 0000000..52c6cb3 --- /dev/null +++ b/mqtt-test-client/src/main/resources/application.yml @@ -0,0 +1,8 @@ +case: ReportTest + +mqtt: + host: 127.0.0.1 + port: 1883 + +start: 0 +end: 100 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e16fda1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + + + + + + + emqx-plugin + + + + + org.springframework.boot + spring-boot-starter-parent + 2.7.11 + + + 2.10.19 + + cc.iotkit.plugins + iot-iita-plugins + pom + + + 11 + 2.7.11 + 3.1.4 + 4.2.2 + + + + + + cc.iotkit + iot-plugin-core + 1.0.5 + + + + com.gitee.starblues + spring-brick + ${spring-brick.version} + + + + com.gitee.starblues + spring-brick-bootstrap + ${spring-brick.version} + + + cn.hutool + hutool-all + 5.8.16 + + + + org.springframework.boot + spring-boot-dependencies + pom + import + ${spring-boot.version} + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework + spring-web + + + + org.slf4j + slf4j-api + + + + ch.qos.logback + logback-classic + + + + org.projectlombok + lombok + + + + + \ No newline at end of file diff --git a/tcp-plugin/pom.xml b/tcp-plugin/pom.xml new file mode 100644 index 0000000..9230676 --- /dev/null +++ b/tcp-plugin/pom.xml @@ -0,0 +1,87 @@ + + + + iot-iita-plugins + cc.iotkit.plugins + 2.0.19 + + 4.0.0 + + 1.0.4 + tcp-plugin + + + + + io.vertx + vertx-core + ${vertx.version} + + + + io.netty + netty-transport + + + + junit + junit + 4.13.2 + test + + + + + + + dev + + true + + + dev + + + + + prod + + prod + + + + + + + + com.gitee.starblues + spring-brick-maven-packager + ${spring-brick.version} + + ${plugin.build.mode} + + tcp-plugin + cc.iotkit.plugins.tcp.Application + ${project.version} + iita + tcp示例插件 + application.yml + + + jar + + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/Application.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/Application.java new file mode 100644 index 0000000..d28d3d2 --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/Application.java @@ -0,0 +1,21 @@ +package cc.iotkit.plugins.tcp; + +import com.gitee.starblues.bootstrap.SpringPluginBootstrap; +import com.gitee.starblues.bootstrap.annotation.OneselfConfig; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @author sjg + */ +@SpringBootApplication(scanBasePackages = "cc.iotkit.plugins.tcp") +@OneselfConfig(mainConfigFileName = {"application.yml"}) +@EnableConfigurationProperties +@EnableScheduling +public class Application extends SpringPluginBootstrap { + + public static void main(String[] args) { + new Application().run(Application.class, args); + } +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/cilent/VertxTcpClient.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/cilent/VertxTcpClient.java new file mode 100644 index 0000000..285b365 --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/cilent/VertxTcpClient.java @@ -0,0 +1,95 @@ +package cc.iotkit.plugins.tcp.cilent; + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Hex; + +import java.time.Duration; + +/** + * @author huangwenl + * @date 2022-10-13 + */ +@Slf4j +public class VertxTcpClient { + @Getter + private String id; + public NetSocket socket; + @Setter + private long keepAliveTimeoutMs = Duration.ofSeconds(30).toMillis(); + private volatile long lastKeepAliveTime = System.currentTimeMillis(); + + @Setter + private RecordParser parser; + + public VertxTcpClient(String id) { + this.id = id; + } + + public void keepAlive() { + lastKeepAliveTime = System.currentTimeMillis(); + } + + public boolean isOnline() { + return System.currentTimeMillis() - lastKeepAliveTime < keepAliveTimeoutMs; + } + + public void setSocket(NetSocket socket) { + synchronized (this) { + if (this.socket != null && this.socket != socket) { + this.socket.close(); + } + + this.socket = socket + .closeHandler(v -> shutdown()) + .handler(buffer -> { + if (log.isDebugEnabled()) { + log.debug("handle tcp client[{}] payload:[{}]", + socket.remoteAddress(), + Hex.encodeHexString(buffer.getBytes())); + } + keepAlive(); + parser.handle(buffer); + if (this.socket != socket) { + log.warn("tcp client [{}] memory leak ", socket.remoteAddress()); + socket.close(); + } + }); + } + } + + public void shutdown() { + log.debug("tcp client [{}] disconnect", getId()); + synchronized (this) { + if (null != socket) { + execute(socket::close); + this.socket = null; + } + } + } + + public void sendMessage(Buffer buffer) { + log.info("wirte data:{}", buffer.toString()); + socket.write(buffer, r -> { + keepAlive(); + if (r.succeeded()) { + log.info("client msg send success:{}", buffer.toString()); + } else { + log.error("client msg send failed", r.cause()); + } + }); + } + + private void execute(Runnable runnable) { + try { + runnable.run(); + } catch (Exception e) { + log.warn("close tcp client error", e); + } + } + +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/BeanConfig.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/BeanConfig.java new file mode 100644 index 0000000..71b04ef --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/BeanConfig.java @@ -0,0 +1,38 @@ +package cc.iotkit.plugins.tcp.conf; + +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.IPluginScript; +import cc.iotkit.plugin.core.LocalPluginConfig; +import cc.iotkit.plugin.core.LocalPluginScript; +import cc.iotkit.plugin.core.thing.IThingService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @author sjg + */ +@Slf4j +@Component +public class BeanConfig { + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IThingService getThingService() { + return new FakeThingService(); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginScript getPluginScript() { + log.info("init LocalPluginScript"); + return new LocalPluginScript("script.js"); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginConfig getPluginConfig() { + return new LocalPluginConfig(); + } +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/FakeThingService.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/FakeThingService.java new file mode 100644 index 0000000..bf0e2e4 --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/FakeThingService.java @@ -0,0 +1,47 @@ +package cc.iotkit.plugins.tcp.conf; + +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * 测试服务 + * + * @author sjg + */ +@Slf4j +public class FakeThingService implements IThingService { + + @Override + public ActionResult post(String pluginId, IDeviceAction action) { + log.info("post action:{}", action); + return ActionResult.builder().code(0).build(); + } + + @Override + public ThingProduct getProduct(String pk) { + return ThingProduct.builder() + .productKey("cGCrkK7Ex4FESAwe") + .productSecret("aaaaaaaa") + .build(); + } + + @Override + public ThingDevice getDevice(String dn) { + return ThingDevice.builder() + .productKey("cGCrkK7Ex4FESAwe") + .deviceName(dn) + .build(); + } + + @Override + public Map getProperty(String dn) { + return new HashMap<>(0); + } +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/TcpServerConfig.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/TcpServerConfig.java new file mode 100644 index 0000000..0bf07ab --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/TcpServerConfig.java @@ -0,0 +1,34 @@ +package cc.iotkit.plugins.tcp.conf; + +import io.vertx.core.net.SocketAddress; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author huangwenl + * @date 2022-10-13 + */ +@Data +@Component +@ConfigurationProperties(prefix = "tcp") +public class TcpServerConfig { + + private String host; + + private int port; + + /** + * 服务实例数量(线程数) + */ + private int instance = Runtime.getRuntime().availableProcessors(); + + public SocketAddress createSocketAddress() { + if (StringUtils.isEmpty(host)) { + host = "localhost"; + } + return SocketAddress.inetSocketAddress(port, host); + } + +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataDecoder.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataDecoder.java new file mode 100644 index 0000000..59fd31d --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataDecoder.java @@ -0,0 +1,23 @@ +package cc.iotkit.plugins.tcp.parser; + +import io.vertx.core.buffer.Buffer; +import lombok.extern.slf4j.Slf4j; + +/** + * 数据解码 + * + * @author sjg + */ +@Slf4j +public class DataDecoder { + + public static DataPackage decode(Buffer buffer) { + DataPackage data = new DataPackage(); + data.setAddr(buffer.getBuffer(0, 6).toString()); + data.setCode(buffer.getShort(6)); + data.setMid(buffer.getShort(8)); + data.setPayload(buffer.getBytes(10, buffer.length())); + return data; + } + +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataEncoder.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataEncoder.java new file mode 100644 index 0000000..88d2dd1 --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataEncoder.java @@ -0,0 +1,24 @@ +package cc.iotkit.plugins.tcp.parser; + +import io.vertx.core.buffer.Buffer; +import lombok.extern.slf4j.Slf4j; + +/** + * 数据编码 + * + * @author sjg + */ +@Slf4j +public class DataEncoder { + + public static Buffer encode(DataPackage data) { + Buffer buffer = Buffer.buffer(); + //设备地址(6byte) + 功能码(2byte) + 消息序号(2byte) + 包体(不定长度) + buffer.appendInt(6+2+2+data.getPayload().length); + buffer.appendBytes(data.getAddr().getBytes()); + buffer.appendShort(data.getCode()); + buffer.appendShort(data.getMid()); + buffer.appendBytes(data.getPayload()); + return buffer; + } +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataPackage.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataPackage.java new file mode 100644 index 0000000..e749a89 --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataPackage.java @@ -0,0 +1,61 @@ +package cc.iotkit.plugins.tcp.parser; + +import cn.hutool.core.util.HexUtil; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.IOException; + +/** + * 数据包 + * + * @author sjg + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class DataPackage { + + public static final short CODE_REGISTER = 0x10; + public static final short CODE_REGISTER_REPLY = 0x11; + public static final short CODE_HEARTBEAT = 0x20; + public static final short CODE_DATA_UP = 0x30; + public static final short CODE_DATA_DOWN = 0x40; + + /** + * 设备地址 + */ + private String addr; + + /** + * 功能码 + */ + private short code; + + /** + * 消息序号 + */ + private short mid; + + /** + * 包体数据 + */ + @JsonSerialize(using = BufferSerializer.class) + private byte[] payload; + + + public static class BufferSerializer extends JsonSerializer { + + @Override + public void serialize(byte[] value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeString(HexUtil.encodeHexStr(value)); + } + } +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataReader.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataReader.java new file mode 100644 index 0000000..9961863 --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataReader.java @@ -0,0 +1,44 @@ +package cc.iotkit.plugins.tcp.parser; + +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; + +import java.util.function.Consumer; + +/** + * 数据包读取器 + * + * @author sjg + */ +public class DataReader { + + public static RecordParser getParser(Consumer receiveHandler) { + RecordParser parser = RecordParser.newFixed(4); + // 设置处理器 + parser.setOutput(new Handler<>() { + // 表示当前数据长度 + int size = -1; + + @Override + public void handle(Buffer buffer) { + //-1表示当前还没有长度信息,需要从收到的数据中取出长度 + if (-1 == size) { + //取出长度 + size = buffer.getInt(0); + //动态修改长度 + parser.fixedSizeMode(size); + } else { + //如果size != -1, 说明已经接受到长度信息了,接下来的数据就是protobuf可识别的字节数组 + byte[] buf = buffer.getBytes(); + receiveHandler.accept(Buffer.buffer(buf)); + //处理完后要将长度改回 + parser.fixedSizeMode(4); + //重置size变量 + size = -1; + } + } + }); + return parser; + } +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpDevice.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpDevice.java new file mode 100644 index 0000000..de653b5 --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpDevice.java @@ -0,0 +1,88 @@ +package cc.iotkit.plugins.tcp.server; + +import cc.iotkit.common.enums.ErrCode; +import cc.iotkit.common.exception.BizException; +import cc.iotkit.plugin.core.thing.IDevice; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.down.DeviceConfig; +import cc.iotkit.plugin.core.thing.actions.down.PropertyGet; +import cc.iotkit.plugin.core.thing.actions.down.PropertySet; +import cc.iotkit.plugin.core.thing.actions.down.ServiceInvoke; +import cc.iotkit.plugins.tcp.parser.DataEncoder; +import cc.iotkit.plugins.tcp.parser.DataPackage; +import cc.iotkit.script.IScriptEngine; +import cn.hutool.core.util.HexUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import io.vertx.core.buffer.Buffer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * tcp设备下行接口 + * + * @author sjg + */ +@Service +public class TcpDevice implements IDevice { + + @Autowired + private TcpServerVerticle tcpServerVerticle; + + private final AtomicInteger atMid = new AtomicInteger(0); + + @Override + public ActionResult config(DeviceConfig action) { + return ActionResult.builder().code(0).reason("").build(); + } + + @Override + public ActionResult propertyGet(PropertyGet action) { + throw new UnsupportedOperationException(); + } + + @Override + public ActionResult propertySet(PropertySet action) { + IScriptEngine scriptEngine = tcpServerVerticle.getScriptEngine(); + //使用转换脚本转换参数部分内容 + String payload = scriptEngine.invokeMethod(new TypeReference<>() { + }, "encode", action.getParams()); + + if (payload == null) { + return ActionResult.builder().code(ErrCode.MSG_CONVERT_ERROR.getKey()).build(); + } + + if (atMid.compareAndSet(Short.MAX_VALUE / 2 - 1, 0)) { + atMid.set(0); + } + byte[] bytes = HexUtil.decodeHex(payload); + + //构造数据包 + DataPackage data = DataPackage.builder() + .addr(action.getDeviceName()) + .code(DataPackage.CODE_DATA_DOWN) + .mid((short) atMid.getAndIncrement()) + .payload(bytes) + .build(); + + return send(action.getDeviceName(), DataEncoder.encode(data)); + } + + @Override + public ActionResult serviceInvoke(ServiceInvoke action) { + throw new UnsupportedOperationException(); + } + + private ActionResult send(String deviceName, Buffer msg) { + try { + tcpServerVerticle.sendMsg(deviceName, msg); + return ActionResult.builder().code(0).reason("").build(); + } catch (BizException e) { + return ActionResult.builder().code(e.getCode()).reason(e.getMessage()).build(); + } catch (Exception e) { + return ActionResult.builder().code(ErrCode.UNKNOWN_EXCEPTION.getKey()).reason(e.getMessage()).build(); + } + } + +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpPlugin.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpPlugin.java new file mode 100644 index 0000000..2a8b675 --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpPlugin.java @@ -0,0 +1,97 @@ +package cc.iotkit.plugins.tcp.server; + +import cc.iotkit.plugin.core.IPlugin; +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugins.tcp.conf.TcpServerConfig; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.bootstrap.realize.PluginCloseListener; +import com.gitee.starblues.core.PluginCloseType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * tcp插件 + * + * @author sjg + */ +@Slf4j +@Service +public class TcpPlugin implements PluginCloseListener, IPlugin { + + @Autowired + private PluginInfo pluginInfo; + @Autowired + private TcpServerVerticle tcpServerVerticle; + + @Autowired + private TcpServerConfig tcpConfig; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginConfig pluginConfig; + + private Vertx vertx; + private String deployedId; + + @PostConstruct + public void init() { + vertx = Vertx.vertx(); + try { + //获取插件最新配置替换当前配置 + Map config = pluginConfig.getConfig(pluginInfo.getPluginId()); + BeanUtil.copyProperties(config, tcpConfig, CopyOptions.create().ignoreNullValue()); + tcpServerVerticle.setConfig(tcpConfig); + + Future future = vertx.deployVerticle(tcpServerVerticle); + future.onSuccess((s -> { + deployedId = s; + log.info("tcp plugin started success"); + })); + future.onFailure(Throwable::printStackTrace); + } catch (Throwable e) { + log.error("init tcp plugin error", e); + } + } + + @SneakyThrows + @Override + public void close(GenericApplicationContext applicationContext, PluginInfo pluginInfo, PluginCloseType closeType) { + log.info("plugin close,type:{},pluginId:{}", closeType, pluginInfo.getPluginId()); + try { + if (deployedId != null) { + CountDownLatch wait = new CountDownLatch(1); + Future future = vertx.undeploy(deployedId); + future.onSuccess(unused -> { + log.info("tcp plugin stopped success"); + wait.countDown(); + }); + future.onFailure(h -> { + log.info("tcp plugin stopped failed"); + h.printStackTrace(); + wait.countDown(); + }); + wait.await(5, TimeUnit.SECONDS); + } + } catch (Throwable e) { + log.error("close plugin error", e); + } + } + + @Override + public Map getLinkInfo(String pk, String dn) { + return null; + } +} diff --git a/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpServerVerticle.java b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpServerVerticle.java new file mode 100644 index 0000000..df1224b --- /dev/null +++ b/tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpServerVerticle.java @@ -0,0 +1,266 @@ +package cc.iotkit.plugins.tcp.server; + + +import cc.iotkit.common.exception.BizException; +import cc.iotkit.plugin.core.IPluginScript; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.DeviceState; +import cc.iotkit.plugin.core.thing.actions.up.DeviceRegister; +import cc.iotkit.plugin.core.thing.actions.up.DeviceStateChange; +import cc.iotkit.plugin.core.thing.actions.up.PropertyReport; +import cc.iotkit.plugins.tcp.cilent.VertxTcpClient; +import cc.iotkit.plugins.tcp.conf.TcpServerConfig; +import cc.iotkit.plugins.tcp.parser.DataDecoder; +import cc.iotkit.plugins.tcp.parser.DataEncoder; +import cc.iotkit.plugins.tcp.parser.DataPackage; +import cc.iotkit.plugins.tcp.parser.DataReader; +import cc.iotkit.script.IScriptEngine; +import cn.hutool.core.util.IdUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetServer; +import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * @author huangwenl + * @date 2022-10-13 + */ +@Slf4j +@Service +public class TcpServerVerticle extends AbstractVerticle { + + @Getter + @Setter + private TcpServerConfig config; + + private final Map clientMap = new ConcurrentHashMap<>(); + + private final Map dnToPk = new HashMap<>(); + + private final Map heartbeatDevice = new HashMap<>(); + + @Setter + private long keepAliveTimeout = Duration.ofSeconds(30).toMillis(); + + private NetServer netServer; + + @Getter + private IScriptEngine scriptEngine; + + @Autowired + private PluginInfo pluginInfo; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginScript pluginScript; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + + @Override + public void stop() { + if (netServer != null) { + netServer.close(rst -> { + log.info("tcp server close:{}", rst.succeeded()); + }); + } + + log.info("tcp server stopped"); + } + + /** + * 创建配置文件 + */ + @PostConstruct + public void initConfig() { + log.info("initConfig:{}", pluginScript.getClass().getName()); + //获取脚本引擎 + scriptEngine = pluginScript.getScriptEngine(pluginInfo.getPluginId()); + if (scriptEngine == null) { + throw new BizException("script engine is null"); + } + Executors.newSingleThreadScheduledExecutor().schedule(this::initTcpServer, 3, TimeUnit.SECONDS); + } + + + /** + * 初始TCP服务 + */ + private void initTcpServer() { + netServer = vertx.createNetServer( + new NetServerOptions().setHost(config.getHost()) + .setPort(config.getPort())); + netServer.connectHandler(this::acceptTcpConnection); + netServer.listen(config.createSocketAddress(), result -> { + if (result.succeeded()) { + log.info("tcp server startup on {}", result.result().actualPort()); + } else { + result.cause().printStackTrace(); + } + }); + } + + public void sendMsg(String addr, Buffer msg) { + VertxTcpClient tcpClient = clientMap.get(addr); + if (tcpClient != null) { + tcpClient.sendMessage(msg); + } + } + + @Scheduled(fixedRate = 40, timeUnit = TimeUnit.SECONDS) + private void offlineCheckTask() { + log.info("keepClientTask"); + Set clients = new HashSet<>(clientMap.keySet()); + for (String key : clients) { + VertxTcpClient client = clientMap.get(key); + if (!client.isOnline()) { + client.shutdown(); + } + } + + heartbeatDevice.keySet().iterator().forEachRemaining(addr -> { + Long time = heartbeatDevice.get(addr); + //心跳超时,判定为离线 + if (System.currentTimeMillis() - time > keepAliveTimeout * 2) { + heartbeatDevice.remove(addr); + //离线上报 + thingService.post(pluginInfo.getPluginId(), DeviceStateChange.builder() + .id(IdUtil.simpleUUID()) + .productKey(dnToPk.get(addr)) + .deviceName(addr) + .state(DeviceState.OFFLINE) + .time(System.currentTimeMillis()) + .build()); + } + }); + } + + /** + * TCP连接处理逻辑 + * + * @param socket socket + */ + protected void acceptTcpConnection(NetSocket socket) { + // 客户端连接处理 + String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress(); + VertxTcpClient client = new VertxTcpClient(clientId); + client.setKeepAliveTimeoutMs(keepAliveTimeout); + try { + // TCP异常和关闭处理 + socket.exceptionHandler(Throwable::printStackTrace).closeHandler(nil -> { + log.debug("tcp server client [{}] closed", socket.remoteAddress()); + client.shutdown(); + }); + // 这个地方是在TCP服务初始化的时候设置的 parserSupplier + client.setKeepAliveTimeoutMs(keepAliveTimeout); + client.setSocket(socket); + RecordParser parser = DataReader.getParser(buffer -> { + try { + DataPackage data = DataDecoder.decode(buffer); + String addr = data.getAddr(); + int code = data.getCode(); + if (code == DataPackage.CODE_REGISTER) { + clientMap.put(addr, client); + heartbeatDevice.remove(addr); + //设备注册 + String pk = new String(data.getPayload()); + dnToPk.put(addr, pk); + ActionResult rst = thingService.post(pluginInfo.getPluginId(), DeviceRegister.builder() + .id(IdUtil.simpleUUID()) + .productKey(pk) + .deviceName(addr) + .time(System.currentTimeMillis()) + .build()); + if (rst.getCode() == 0) { + //回复注册成功给客户端 + sendMsg(addr, DataEncoder.encode( + DataPackage.builder() + .addr(addr) + .code(DataPackage.CODE_REGISTER_REPLY) + .mid(data.getMid()) + .payload(Buffer.buffer().appendInt(0).getBytes()) + .build() + )); + } + return; + } + + if (code == DataPackage.CODE_HEARTBEAT) { + //心跳 + online(addr); + heartbeatDevice.put(addr, System.currentTimeMillis()); + return; + } + + if (code == DataPackage.CODE_DATA_UP) { + //设备数据上报 + online(addr); + //数据上报也作为心跳 + heartbeatDevice.put(addr, System.currentTimeMillis()); + //这里可以直接解码,或调用脚本解码(用脚本解码不用修改程序重新打包) + Map rst = scriptEngine.invokeMethod(new TypeReference<>() { + }, "decode", data); + if (rst == null) { + return; + } + //属性上报 + thingService.post(pluginInfo.getPluginId(), PropertyReport.builder() + .id(IdUtil.simpleUUID()) + .productKey(dnToPk.get(addr)) + .deviceName(addr) + .params(rst) + .time(System.currentTimeMillis()) + .build()); + } + + //未注册断开连接 + if (!clientMap.containsKey(data.getAddr())) { + socket.close(); + } + + } catch (Exception e) { + e.printStackTrace(); + } + }); + client.setParser(parser); + log.debug("accept tcp client [{}] connection", socket.remoteAddress()); + } catch (Exception e) { + e.printStackTrace(); + client.shutdown(); + } + } + + private void online(String addr) { + if (!heartbeatDevice.containsKey(addr)) { + //第一次心跳,上线 + thingService.post(pluginInfo.getPluginId(), DeviceStateChange.builder() + .id(IdUtil.simpleUUID()) + .productKey(dnToPk.get(addr)) + .deviceName(addr) + .state(DeviceState.ONLINE) + .time(System.currentTimeMillis()) + .build()); + } + } + +} diff --git a/tcp-plugin/src/main/resources/application.yml b/tcp-plugin/src/main/resources/application.yml new file mode 100644 index 0000000..e1d8e8f --- /dev/null +++ b/tcp-plugin/src/main/resources/application.yml @@ -0,0 +1,7 @@ +plugin: + runMode: prod + mainPackage: cc.iotkit.plugin + +tcp: + host: 127.0.0.1 + port: 6883 diff --git a/tcp-plugin/src/main/resources/config.json b/tcp-plugin/src/main/resources/config.json new file mode 100644 index 0000000..50753b8 --- /dev/null +++ b/tcp-plugin/src/main/resources/config.json @@ -0,0 +1,16 @@ +[ + { + "id": "host", + "name": "绑定ip", + "type": "text", + "value": "127.0.0.1", + "desc": "tcp绑定ip,默认为127.0.0.1" + }, + { + "id": "port", + "name": "端口", + "type": "number", + "value": 6883, + "desc": "tcp端口,默认为6883" + } +] \ No newline at end of file diff --git a/tcp-plugin/src/main/resources/script.js b/tcp-plugin/src/main/resources/script.js new file mode 100644 index 0000000..463c800 --- /dev/null +++ b/tcp-plugin/src/main/resources/script.js @@ -0,0 +1,33 @@ +function hexToByte(hexString) { + if (hexString.length % 2 !== 0) { + throw new Error('Invalid hex string. String must have an even number of characters.'); + } + + let byteArray = []; + for (let i = 0; i < hexString.length; i += 4) { + byteArray.push(parseInt(hexString.substr(i, 4), 16)); + } + + return byteArray; +} +function byteToHex(bytes) { + for (var hex = [], i = 0; i < bytes.length; i++) { + hex.push((bytes[i] >>> 4).toString(16)); + hex.push((bytes[i] & 0xF).toString(16)); + } + return hex.join(""); +} + +this.decode=function(data){ + hex=data.payload; + const bytes=hexToByte(hex); + return { + "rssi":bytes[0], + "powerstate":bytes[1] + }; +} + +this.encode=function(params){ + const hex=byteToHex([params.powerstate]); + return hex; +} \ No newline at end of file diff --git a/tcp-plugin/src/test/java/cc/iotkit/test/ScriptTest.java b/tcp-plugin/src/test/java/cc/iotkit/test/ScriptTest.java new file mode 100644 index 0000000..7d10556 --- /dev/null +++ b/tcp-plugin/src/test/java/cc/iotkit/test/ScriptTest.java @@ -0,0 +1,26 @@ +package cc.iotkit.test; + +import cc.iotkit.plugin.core.LocalPluginScript; +import cc.iotkit.script.IScriptEngine; +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.Before; +import org.junit.Test; + +import java.util.Map; + +public class ScriptTest { + IScriptEngine scriptEngine; + + @Before + public void init() { + scriptEngine = new LocalPluginScript("script.js").getScriptEngine(""); + } + + @Test + public void testEncode() { + String rst = scriptEngine.invokeMethod(new TypeReference<>() { + }, "encode", Map.of("powerstate", 1)); + System.out.println(rst); + } + +} diff --git a/tcp-plugin/src/test/java/cc/iotkit/test/TcpClientTest.java b/tcp-plugin/src/test/java/cc/iotkit/test/TcpClientTest.java new file mode 100644 index 0000000..6d35aa0 --- /dev/null +++ b/tcp-plugin/src/test/java/cc/iotkit/test/TcpClientTest.java @@ -0,0 +1,21 @@ +package cc.iotkit.test; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TcpClientTest { + + public static void main(String[] args) { + Vertx vertx = Vertx.vertx(); + Future future = vertx.deployVerticle(new TcpClientVerticle()); + future.onSuccess((s -> { + log.info("tcp client started success"); + })); + future.onFailure((e) -> { + log.error("tcp client startup failed", e); + }); + } + +} diff --git a/tcp-plugin/src/test/java/cc/iotkit/test/TcpClientVerticle.java b/tcp-plugin/src/test/java/cc/iotkit/test/TcpClientVerticle.java new file mode 100644 index 0000000..77da006 --- /dev/null +++ b/tcp-plugin/src/test/java/cc/iotkit/test/TcpClientVerticle.java @@ -0,0 +1,132 @@ +package cc.iotkit.test; + +import cc.iotkit.common.utils.ThreadUtil; +import cc.iotkit.plugins.tcp.parser.DataDecoder; +import cc.iotkit.plugins.tcp.parser.DataEncoder; +import cc.iotkit.plugins.tcp.parser.DataPackage; +import cc.iotkit.plugins.tcp.parser.DataReader; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.RandomUtil; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetClient; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author huangwenlong + * @version 1.0 + * @date 2022/10/23 13:08 + */ +@Slf4j +public class TcpClientVerticle extends AbstractVerticle { + + private NetClient netClient; + + private NetSocket socket; + + private String addr = "060101"; + + private String pk = "cGCrkK7Ex4FESAwe"; + + private AtomicInteger atMid = new AtomicInteger(0); + + @Override + public void start() { + initClient(); + } + + @Override + public void stop() { + if (null != netClient) { + netClient.close(); + } + } + + private void initClient() { + NetClientOptions options = new NetClientOptions(); + options.setReconnectAttempts(Integer.MAX_VALUE); + options.setReconnectInterval(20000L); + netClient = vertx.createNetClient(options); + RecordParser parser = DataReader.getParser(this::handle); + + netClient.connect(6883, "127.0.0.1", result -> { + if (result.succeeded()) { + log.debug("connect tcp success"); + socket = result.result(); + socket.handler(parser); + //注册 + byte[] pkBytes = pk.getBytes(); + send(DataPackage.builder() + .addr(addr) + .code(DataPackage.CODE_REGISTER) + .mid(getMid()) + .payload(pkBytes) + .build()); + } else { + log.error("connect tcp error", result.cause()); + } + }); + } + + private short getMid() { + atMid.compareAndSet(254, 0); + return (short) atMid.getAndIncrement(); + } + + private void send(DataPackage data) { + Buffer buffer = DataEncoder.encode(data); + log.info("send data:{}", HexUtil.encodeHexStr(buffer.getBytes())); + socket.write(buffer); + } + + public void handle(Buffer buffer) { + log.info("receive server data:{}", buffer.toString()); + DataPackage data = DataDecoder.decode(buffer); + if (data.getCode() == DataPackage.CODE_REGISTER_REPLY) { + int rst = Buffer.buffer(data.getPayload()).getInt(0); + if (rst == 0) { + log.info("device:{} register success", data.getAddr()); + //定时心跳 + ThreadUtil.newScheduled(1, "heartbeat") + .scheduleWithFixedDelay(this::heartbeat, 10, 30, TimeUnit.SECONDS); + //随机上报数据 + ThreadUtil.newScheduled(1, "reportData") + .scheduleWithFixedDelay(this::reportData, 20, 1, TimeUnit.SECONDS); + } + } + } + + private void heartbeat() { + send(DataPackage.builder() + .addr(addr) + .code(DataPackage.CODE_HEARTBEAT) + .mid(getMid()) + .build()); + } + + private void reportData() { + if (RandomUtil.randomInt() % 3 == 0) { + //随机 + return; + } + send(DataPackage.builder() + .addr(addr) + .code(DataPackage.CODE_DATA_UP) + .mid(getMid()) + .payload(Buffer.buffer() + //rssi + .appendShort((short) RandomUtil.randomInt(0, 127)) + //powerstate + .appendByte((byte) (RandomUtil.randomInt() % 2 == 0 ? 1 : 0)) + .getBytes() + ) + .build()); + } + +} \ No newline at end of file diff --git a/tcp-plugin/src/test/resources/script.js b/tcp-plugin/src/test/resources/script.js new file mode 100644 index 0000000..463c800 --- /dev/null +++ b/tcp-plugin/src/test/resources/script.js @@ -0,0 +1,33 @@ +function hexToByte(hexString) { + if (hexString.length % 2 !== 0) { + throw new Error('Invalid hex string. String must have an even number of characters.'); + } + + let byteArray = []; + for (let i = 0; i < hexString.length; i += 4) { + byteArray.push(parseInt(hexString.substr(i, 4), 16)); + } + + return byteArray; +} +function byteToHex(bytes) { + for (var hex = [], i = 0; i < bytes.length; i++) { + hex.push((bytes[i] >>> 4).toString(16)); + hex.push((bytes[i] & 0xF).toString(16)); + } + return hex.join(""); +} + +this.decode=function(data){ + hex=data.payload; + const bytes=hexToByte(hex); + return { + "rssi":bytes[0], + "powerstate":bytes[1] + }; +} + +this.encode=function(params){ + const hex=byteToHex([params.powerstate]); + return hex; +} \ No newline at end of file diff --git a/websocket-plugin/pom.xml b/websocket-plugin/pom.xml new file mode 100644 index 0000000..f8f72fe --- /dev/null +++ b/websocket-plugin/pom.xml @@ -0,0 +1,74 @@ + + + + iot-iita-plugins + cc.iotkit.plugins + 2.0.19 + + 4.0.0 + + websocket-plugin + + + + + io.vertx + vertx-core + ${vertx.version} + + + + + + + dev + + true + + + dev + + + + + prod + + prod + + + + + + + + com.gitee.starblues + spring-brick-maven-packager + ${spring-brick.version} + + ${plugin.build.mode} + + websocket-plugin + cc.iotkit.plugins.websocket.Application + ${project.version} + iita + websocket示例插件 + application.yml + + + jar + + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/Application.java b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/Application.java new file mode 100644 index 0000000..ce5aee8 --- /dev/null +++ b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/Application.java @@ -0,0 +1,19 @@ +package cc.iotkit.plugins.websocket; + +import com.gitee.starblues.bootstrap.SpringPluginBootstrap; +import com.gitee.starblues.bootstrap.annotation.OneselfConfig; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * @author tfd + */ +@SpringBootApplication(scanBasePackages = "cc.iotkit.plugins.websocket") +@OneselfConfig(mainConfigFileName = {"application.yml"}) +@EnableConfigurationProperties +public class Application extends SpringPluginBootstrap { + + public static void main(String[] args) { + new Application().run(Application.class, args); + } +} diff --git a/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/analysis/DataAnalysis.java b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/analysis/DataAnalysis.java new file mode 100644 index 0000000..3832fec --- /dev/null +++ b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/analysis/DataAnalysis.java @@ -0,0 +1,38 @@ +package cc.iotkit.plugins.websocket.analysis; + +import cc.iotkit.common.utils.JsonUtils; +import cc.iotkit.plugins.websocket.beans.Event; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author:tfd + * @Date:2024/4/11 15:42 + */ +@Slf4j +public class DataAnalysis { + public static final String EVENT_STATE_CHANGED="state_changed"; + public static final ObjectMapper mapper = new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + + public static Map stateChangedEvent(Event.StateData oldState, Event.StateData newState){ + Map ret= new HashMap<>(); + if(!oldState.getState().equals(newState.getState())){ + ret.put("state",newState.getState()); + } + HashMap oldAttributes=JsonUtils.parseObject(JsonUtils.toJsonString(oldState.getAttributes()),HashMap.class); + HashMap newAttributes=JsonUtils.parseObject(JsonUtils.toJsonString(newState.getAttributes()),HashMap.class); + newAttributes.forEach((key, value) -> { + if(ObjectUtil.isNotNull(value)&&!JsonUtils.toJsonString(oldAttributes.get(key)).equals(JsonUtils.toJsonString(newAttributes.get(key)))){ + ret.put(StrUtil.toCamelCase(key),value); + } + }); + log.info("analysis:{}",JsonUtils.toJsonString(ret)); + return ret; + } +} diff --git a/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/beans/Event.java b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/beans/Event.java new file mode 100644 index 0000000..aa4da5d --- /dev/null +++ b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/beans/Event.java @@ -0,0 +1,34 @@ +package cc.iotkit.plugins.websocket.beans; + +import lombok.Data; + +/** + * @Author:tfd + * @Date:2024/4/11 16:57 + */ +@Data +public class Event { + private String eventType; + private EventData data; + private String origin; + private String timeFired; + private Object context; + + @Data + public class EventData{ + private String entityId; + private StateData oldState; + private StateData newState; + } + + @Data + public static class StateData{ + private String entityId; + private String state; + private Object attributes; + private String lastChanged; + private String lastUpdated; + private Object context; + } + +} diff --git a/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/conf/BeanConfig.java b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/conf/BeanConfig.java new file mode 100644 index 0000000..b10e798 --- /dev/null +++ b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/conf/BeanConfig.java @@ -0,0 +1,28 @@ +package cc.iotkit.plugins.websocket.conf; + +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugin.core.LocalPluginConfig; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugins.websocket.service.FakeThingService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * @author tfd + */ +@Component +public class BeanConfig { + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IThingService getThingService() { + return new FakeThingService(); + } + + @Bean + @ConditionalOnProperty(name = "plugin.runMode", havingValue = "dev") + IPluginConfig getPluginConfig(){ + return new LocalPluginConfig(); + } +} diff --git a/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/conf/WebsocketConfig.java b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/conf/WebsocketConfig.java new file mode 100644 index 0000000..e4bb7eb --- /dev/null +++ b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/conf/WebsocketConfig.java @@ -0,0 +1,33 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.plugins.websocket.conf; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "websocket") +public class WebsocketConfig { + + private int port; + + private String sslKey; + + private String sslCert; + + private boolean ssl; + + private String tokenKey; + + private String accessToken; + +} diff --git a/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/FakeThingService.java b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/FakeThingService.java new file mode 100644 index 0000000..0b64fd0 --- /dev/null +++ b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/FakeThingService.java @@ -0,0 +1,53 @@ +package cc.iotkit.plugins.websocket.service; + +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.IDeviceAction; +import cc.iotkit.plugin.core.thing.model.ThingDevice; +import cc.iotkit.plugin.core.thing.model.ThingProduct; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * 测试服务 + * + * @author tfd + */ +@Slf4j +public class FakeThingService implements IThingService { + + + /** + * 添加测试设备 + */ + private static final Map DEVICES = new HashMap<>(); + + @Override + public ActionResult post(String pluginId, IDeviceAction action) { + log.info("post action:{}", action); + return ActionResult.builder().code(0).build(); + } + + @Override + public ThingProduct getProduct(String pk) { + return ThingProduct.builder() + .productKey(pk) + .productSecret("") + .build(); + } + + @Override + public ThingDevice getDevice(String dn) { + return ThingDevice.builder() + .productKey(DEVICES.get(dn)) + .deviceName(dn) + .build(); + } + + @Override + public Map getProperty(String dn) { + return new HashMap<>(0); + } +} diff --git a/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketDevice.java b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketDevice.java new file mode 100644 index 0000000..bdd420f --- /dev/null +++ b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketDevice.java @@ -0,0 +1,85 @@ +package cc.iotkit.plugins.websocket.service; + +import cc.iotkit.common.enums.ErrCode; +import cc.iotkit.common.exception.BizException; +import cc.iotkit.plugin.core.thing.IDevice; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.down.DeviceConfig; +import cc.iotkit.plugin.core.thing.actions.down.PropertyGet; +import cc.iotkit.plugin.core.thing.actions.down.PropertySet; +import cc.iotkit.plugin.core.thing.actions.down.ServiceInvoke; +import io.vertx.core.json.JsonObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * websocket设备服务 + * + * @author tfd + */ +@Service +public class WebsocketDevice implements IDevice { + + @Autowired + private WebsocketVerticle websocketVerticle; + + @Override + public ActionResult config(DeviceConfig action) { + return ActionResult.builder().code(0).reason("").build(); + } + + @Override + public ActionResult propertyGet(PropertyGet action) { + return send( + getDeviceKey(action.getDeviceName(),action.getProductKey()), + new JsonObject() + .put("id", action.getId()) + .put("method", "thing.service.property.get") + .put("params", action.getKeys()) + .toString() + ); + } + + @Override + public ActionResult propertySet(PropertySet action) { + return send( + getDeviceKey(action.getDeviceName(),action.getProductKey()), + new JsonObject() + .put("id", action.getId()) + .put("type", "call_service") + .put("params", action.getParams()) + .toString() + ); + } + + @Override + public ActionResult serviceInvoke(ServiceInvoke action) { + return send( + getDeviceKey(action.getDeviceName(),action.getProductKey()), + new JsonObject() + .put("id", action.getId()) + .put("method", "thing.service." + action.getName()) + .put("params", action.getParams()) + .toString() + ); + } + + private ActionResult send(String deviceKey, String payload) { + try { + websocketVerticle.send( + deviceKey, + payload + ); + return ActionResult.builder().code(0).reason("").build(); + } catch (BizException e) { + return ActionResult.builder().code(e.getCode()).reason(e.getMessage()).build(); + } catch (Exception e) { + return ActionResult.builder().code(ErrCode.UNKNOWN_EXCEPTION.getKey()).reason(e.getMessage()).build(); + } + } + + private String getDeviceKey(String deviceName, String productKey) { + return String.format("%s_%s", deviceName, productKey); + } + +} diff --git a/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketPlugin.java b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketPlugin.java new file mode 100644 index 0000000..9db80e5 --- /dev/null +++ b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketPlugin.java @@ -0,0 +1,89 @@ +package cc.iotkit.plugins.websocket.service; + +import cc.iotkit.plugin.core.IPluginConfig; +import cc.iotkit.plugins.websocket.conf.WebsocketConfig; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.bootstrap.realize.PluginCloseListener; +import com.gitee.starblues.core.PluginCloseType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * @author tfd + */ +@Slf4j +@Service +public class WebsocketPlugin implements PluginCloseListener { + + @Autowired + private PluginInfo pluginInfo; + @Autowired + private WebsocketVerticle websocketVerticle; + @Autowired + private WebsocketConfig websocketConfig; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IPluginConfig pluginConfig; + + private Vertx vertx; + private String deployedId; + + @PostConstruct + public void init() { + vertx = Vertx.vertx(); + try { + //获取插件最新配置替换当前配置 + Map config = pluginConfig.getConfig(pluginInfo.getPluginId()); + BeanUtil.copyProperties(config, websocketConfig, CopyOptions.create().ignoreNullValue()); + websocketVerticle.setConfig(websocketConfig); + + Future future = vertx.deployVerticle(websocketVerticle); + future.onSuccess((s -> { + deployedId = s; + log.info("websocket plugin started success"); + })); + future.onFailure((e) -> { + log.error("websocket plugin startup failed", e); + }); + } catch (Throwable e) { + log.error("websocket plugin startup error", e); + } + } + + @Override + public void close(GenericApplicationContext applicationContext, PluginInfo pluginInfo, PluginCloseType closeType) { + try { + log.info("plugin close,type:{},pluginId:{}", closeType, pluginInfo.getPluginId()); + if (deployedId != null) { + CountDownLatch wait = new CountDownLatch(1); + Future future = vertx.undeploy(deployedId); + future.onSuccess(unused -> { + log.info("websocket plugin stopped success"); + wait.countDown(); + }); + future.onFailure(h -> { + log.info("websocket plugin stopped failed"); + h.printStackTrace(); + wait.countDown(); + }); + wait.await(5, TimeUnit.SECONDS); + } + } catch (Throwable e) { + log.error("websocket plugin stop error", e); + } + } + +} diff --git a/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketVerticle.java b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketVerticle.java new file mode 100644 index 0000000..fa486f2 --- /dev/null +++ b/websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketVerticle.java @@ -0,0 +1,301 @@ +/* + * +---------------------------------------------------------------------- + * | Copyright (c) 奇特物联 2021-2022 All rights reserved. + * +---------------------------------------------------------------------- + * | Licensed 未经许可不能去掉「奇特物联」相关版权 + * +---------------------------------------------------------------------- + * | Author: xw2sy@163.com + * +---------------------------------------------------------------------- + */ +package cc.iotkit.plugins.websocket.service; + +import cc.iotkit.common.utils.JsonUtils; +import cc.iotkit.common.utils.StringUtils; +import cc.iotkit.plugin.core.thing.IThingService; +import cc.iotkit.plugin.core.thing.actions.ActionResult; +import cc.iotkit.plugin.core.thing.actions.DeviceState; +import cc.iotkit.plugin.core.thing.actions.up.DeviceRegister; +import cc.iotkit.plugin.core.thing.actions.up.DeviceStateChange; +import cc.iotkit.plugin.core.thing.actions.up.PropertyReport; +import cc.iotkit.plugin.core.thing.actions.up.SubDeviceRegister; +import cc.iotkit.plugins.websocket.analysis.DataAnalysis; +import cc.iotkit.plugins.websocket.beans.Event; +import cc.iotkit.plugins.websocket.conf.WebsocketConfig; +import cn.hutool.core.util.ObjectUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.gitee.starblues.bootstrap.annotation.AutowiredType; +import com.gitee.starblues.core.PluginInfo; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Future; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.ServerWebSocket; +import io.vertx.core.net.PemKeyCertOptions; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * + * @author tfd + */ +@Slf4j +@Component +@Data +public class WebsocketVerticle extends AbstractVerticle { + + private HttpServer httpServer; + private final Map wsClients = new ConcurrentHashMap<>(); + + private static final Map CONNECT_POOL = new ConcurrentHashMap<>(); + private static final Map DEVICE_ONLINE = new ConcurrentHashMap<>(); + + private Map tokens=new HashMap<>(); + + private WebsocketConfig config; + + @Autowired + @AutowiredType(AutowiredType.Type.MAIN_PLUGIN) + private IThingService thingService; + + @Autowired + private PluginInfo pluginInfo; + + @Override + public void start() { + Executors.newSingleThreadScheduledExecutor().schedule(this::initWsServer, 3, TimeUnit.SECONDS); + } + + private void initWsServer() { + HttpServerOptions options = new HttpServerOptions() + .setPort(config.getPort()); + if (config.isSsl()) { + options = options.setSsl(true) + .setKeyCertOptions(new PemKeyCertOptions() + .setKeyPath(config.getSslKey()) + .setCertPath(config.getSslCert())); + } + + httpServer = vertx.createHttpServer(options).webSocketHandler(wsClient -> { + log.info("webSocket client connect sessionId:{},path={}", wsClient.textHandlerID(), wsClient.path()); + String deviceKey = wsClient.path().replace("/",""); + String[] strArr=deviceKey.split("_"); + if(StringUtils.isBlank(deviceKey)||strArr.length!=2){ + log.warn("陌生连接,拒绝"); + wsClient.reject(); + return; + } + wsClient.writeTextMessage("connect succes! please auth!"); + wsClient.textMessageHandler(message -> { + HashMap msg; + try{ + msg=JsonUtils.parseObject(message,HashMap.class); + }catch (Exception e){ + log.warn("数据格式异常"); + wsClient.writeTextMessage("data err"); + return; + } + log.info("webSocket receive message:{}",message); + if(wsClients.containsKey(deviceKey)){ + if("ping".equals(msg.get("type"))){//心跳 + msg.put("type","pong"); + wsClient.writeTextMessage(JsonUtils.toJsonString(msg)); + return; + }else if("register".equals(msg.get("type"))){//注册 + ActionResult result; + List subDevices = null; + if(ObjectUtil.isNotNull(msg.get("subDevices"))){ + subDevices=JsonUtils.parseObject(JsonUtils.toJsonString(msg.get("subDevices")),List.class); + List subsRe =new ArrayList<>(); + for(String sub:subDevices){ + String subName=sub.split("_")[0]; + String subKey=sub.split("_")[1]; + subsRe.add(DeviceRegister.builder() + .productKey(subKey) + .deviceName(subName) + .build()); + } + //带子设备注册 + result = thingService.post( + pluginInfo.getPluginId(), + SubDeviceRegister.builder() + .productKey(strArr[1]) + .deviceName(strArr[0]) + .subs(subsRe) + .build() + ); + }else{ + //设备注册 + result = thingService.post( + pluginInfo.getPluginId(), + DeviceRegister.builder() + .productKey(strArr[1]) + .deviceName(strArr[0]) + .build() + ); + } + if(ObjectUtil.isNotNull(result)&&result.getCode()==0){ + log.info("设备上线"); + //父设备上线 + thingService.post( + pluginInfo.getPluginId(), + DeviceStateChange.builder() + .productKey(strArr[1]) + .deviceName(strArr[0]) + .state(DeviceState.ONLINE) + .build() + ); + //子设备上线 + if(ObjectUtil.isNotNull(subDevices)){ + log.info("子设备上线"); + for(String sub:subDevices){ + String subName=sub.split("_")[0]; + String subKey=sub.split("_")[1]; + thingService.post( + pluginInfo.getPluginId(), + DeviceStateChange.builder() + .productKey(subKey) + .deviceName(subName) + .state(DeviceState.ONLINE) + .build() + ); + + } + } + //注册成功 + Map ret=new HashMap<>(); + ret.put("id",msg.get("id")); + ret.put("type",msg.get("type")); + ret.put("result","succes"); + wsClient.writeTextMessage(JsonUtils.toJsonString(ret)); + return; + }else{ + //注册失败 + Map ret=new HashMap<>(); + ret.put("id",msg.get("id")); + ret.put("type",msg.get("type")); + ret.put("result","fail"); + wsClient.writeTextMessage(JsonUtils.toJsonString(ret)); + return; + } + }else{//数据处理 + if("event".equals(msg.get("type"))){ + Event event= null; + try { + event = DataAnalysis.mapper.readValue(JsonUtils.toJsonString(msg.get("event")), new TypeReference() {}); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + String[] keys=event.getData().getEntityId().split("_"); + if(DataAnalysis.EVENT_STATE_CHANGED.equals(event.getEventType())){ + thingService.post(pluginInfo.getPluginId(), + PropertyReport.builder().productKey(keys[1]) + .deviceName(keys[0]) + .params(DataAnalysis.stateChangedEvent(event.getData() + .getOldState(),event.getData().getNewState())).build()); + + } + //注册失败 + Map ret=new HashMap<>(); + ret.put("id",msg.get("id")); + ret.put("type",msg.get("type")); + ret.put("result","succes"); + wsClient.writeTextMessage(JsonUtils.toJsonString(ret)); + return; + } + } + }else if(msg!=null&&"auth".equals(msg.get("type"))){ + Set tokenKey=tokens.keySet(); + for(String key:tokenKey){ + if(ObjectUtil.isNotNull(msg.get(key))&&tokens.get(key).equals(msg.get(key))){ + //保存设备与连接关系 + log.info("认证通过"); + if(!wsClients.containsKey(deviceKey)){ + wsClients.put(deviceKey, wsClient); + } + wsClient.writeTextMessage("auth succes"); + return; + } + } + log.warn("认证失败,拒绝"); + wsClient.writeTextMessage("auth fail"); + return; + }else{ + log.warn("认证失败,拒绝"); + wsClient.writeTextMessage("auth fail"); + return; + } + + }); + wsClient.closeHandler(c -> { + log.warn("client connection closed,deviceKey:{}", deviceKey); + if(wsClients.containsKey(deviceKey)){ + wsClients.remove(deviceKey); + thingService.post( + pluginInfo.getPluginId(), + DeviceStateChange.builder() + .productKey(strArr[1]) + .deviceName(strArr[0]) + .state(DeviceState.OFFLINE) + .build() + ); + } + }); + wsClient.exceptionHandler(ex -> { + log.warn("webSocket client connection exception,deviceKey:{}", deviceKey); + if(wsClients.containsKey(deviceKey)){ + wsClients.remove(deviceKey); + thingService.post( + pluginInfo.getPluginId(), + DeviceStateChange.builder() + .productKey(strArr[1]) + .deviceName(strArr[0]) + .state(DeviceState.OFFLINE) + .build() + ); + } + }); + }).listen(config.getPort(), server -> { + if (server.succeeded()) { + log.info("webSocket server is listening on port " + config.getPort()); + if(config.getTokenKey()!=null&&config.getAccessToken()!=null){ + tokens.put(config.getTokenKey(),config.getAccessToken()); + } + } else { + log.error("webSocket server on starting the server", server.cause()); + } + }); + } + + @Override + public void stop() { + for (String deviceKey : wsClients.keySet()) { + thingService.post( + pluginInfo.getPluginId(), + DeviceStateChange.builder() + .productKey(deviceKey.split("_")[1]) + .deviceName(deviceKey.split("_")[0]) + .state(DeviceState.OFFLINE) + .build() + ); + } + tokens.clear(); + httpServer.close(voidAsyncResult -> log.info("close webocket server...")); + } + + public void send(String deviceKey,String msg) { + ServerWebSocket wsClient = wsClients.get(deviceKey); + String msgStr = JsonUtils.toJsonString(msg); + log.info("send msg payload:{}", msgStr); + Future result = wsClient.writeTextMessage(msgStr); + result.onFailure(e -> log.error("webSocket server send msg failed", e)); + } + +} diff --git a/websocket-plugin/src/main/resources/application.yml b/websocket-plugin/src/main/resources/application.yml new file mode 100644 index 0000000..122b872 --- /dev/null +++ b/websocket-plugin/src/main/resources/application.yml @@ -0,0 +1,8 @@ +plugin: + runMode: prod + mainPackage: cc.iotkit.plugin + +websocket: + port: 1662 + tokenKey: "test_token" + accessToken: "123456789" diff --git a/websocket-plugin/src/main/resources/config.json b/websocket-plugin/src/main/resources/config.json new file mode 100644 index 0000000..f746ef7 --- /dev/null +++ b/websocket-plugin/src/main/resources/config.json @@ -0,0 +1,23 @@ +[ + { + "id": "port", + "name": "端口", + "type": "number", + "value": 1662, + "desc": "websocket端口,默认为1662" + }, + { + "id": "tokenKey", + "name": "令牌key", + "type": "text", + "value": "test_token", + "desc": "令牌key" + }, + { + "id": "accessToken", + "name": "令牌", + "type": "text", + "value": "123456789", + "desc": "令牌" + } +] \ No newline at end of file