From 34ea26c4b633413096532369165c96dea82c44e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=B1=BC=E5=B9=B2?= <1810377322@163.com> Date: Fri, 6 Jun 2025 18:04:03 +0800 Subject: [PATCH 1/5] Initial commit --- .gitee/ISSUE_TEMPLATE.zh-CN.md | 13 + .gitee/PULL_REQUEST_TEMPLATE.zh-CN.md | 53 ++ .gitignore | 33 ++ DLT645-plugin/pom.xml | 87 +++ .../cc/iotkit/plugins/dlt645/Application.java | 21 + .../dlt645/analysis/DLT645Analysis.java | 323 +++++++++++ .../dlt645/analysis/DLT645Converter.java | 44 ++ .../plugins/dlt645/analysis/DLT645Data.java | 92 ++++ .../dlt645/analysis/DLT645DataFormat.java | 361 +++++++++++++ .../dlt645/analysis/DLT645FunCode.java | 302 +++++++++++ .../dlt645/analysis/DLT645V1997Data.java | 63 +++ .../dlt645/analysis/DLT645V2007Data.java | 66 +++ .../plugins/dlt645/conf/BeanConfig.java | 39 ++ .../plugins/dlt645/conf/TcpClientConfig.java | 21 + .../dlt645/constants/DLT645Constant.java | 10 + .../dlt645/load/DLT645v1997CsvLoader.java | 93 ++++ .../dlt645/load/DLT645v2007CsvLoader.java | 95 ++++ .../plugins/dlt645/service/DLT645Device.java | 54 ++ .../plugins/dlt645/service/Dlt645Plugin.java | 88 +++ .../dlt645/service/FakeThingService.java | 46 ++ .../dlt645/service/TcpClientVerticle.java | 184 +++++++ .../iotkit/plugins/dlt645/utils/ByteRef.java | 15 + .../plugins/dlt645/utils/ByteUtils.java | 78 +++ .../iotkit/plugins/dlt645/utils/BytesRef.java | 15 + .../plugins/dlt645/utils/ContainerUtils.java | 424 +++++++++++++++ .../src/main/resources/DLT645-1997.csv | 143 +++++ .../src/main/resources/DLT645-2007.csv | 425 +++++++++++++++ .../src/main/resources/application.yml | 8 + DLT645-plugin/src/main/resources/config.json | 23 + LICENSE | 201 +++++++ README.en.md | 36 ++ README.md | 37 ++ emqx-plugin/pom.xml | 86 +++ .../cc/iotkit/plugins/emqx/Application.java | 19 + .../plugins/emqx/conf/BCDClockGenerator.java | 39 ++ .../iotkit/plugins/emqx/conf/BeanConfig.java | 28 + .../plugins/emqx/conf/CRC16ModbusUtil.java | 64 +++ .../plugins/emqx/conf/DeviceStatusParser.java | 99 ++++ .../plugins/emqx/conf/EventTypeMapper.java | 53 ++ .../plugins/emqx/conf/GasAlarmDataParser.java | 201 +++++++ .../plugins/emqx/conf/IoTConfigProtocol.java | 197 +++++++ .../iotkit/plugins/emqx/conf/MqttConfig.java | 30 ++ .../plugins/emqx/conf/ProtocolTest.java | 56 ++ .../plugins/emqx/conf/ResponseParserDemo.java | 27 + .../plugins/emqx/handler/IMsgHandler.java | 12 + .../plugins/emqx/service/AuthVerticle.java | 175 ++++++ .../plugins/emqx/service/EmqxPlugin.java | 507 ++++++++++++++++++ .../emqx/service/FakeThingService.java | 70 +++ .../plugins/emqx/service/MqttDevice.java | 186 +++++++ .../src/main/resources/application.yml | 9 + emqx-plugin/src/main/resources/config.json | 30 ++ http-plugin/pom.xml | 81 +++ .../cc/iotkit/plugins/http/Application.java | 19 + .../iotkit/plugins/http/conf/BeanConfig.java | 29 + .../iotkit/plugins/http/conf/HttpConfig.java | 28 + .../http/service/FakeThingService.java | 48 ++ .../plugins/http/service/HttpDevice.java | 43 ++ .../plugins/http/service/HttpPlugin.java | 91 ++++ .../plugins/http/service/HttpVerticle.java | 206 +++++++ .../src/main/resources/application.yml | 6 + http-plugin/src/main/resources/config.json | 26 + http-plugin/src/main/resources/script.js | 0 .../java/cc/iotkit/test/http/HttpTest.java | 36 ++ hydrovalve-plugin/pom.xml | 64 +++ .../plugins/hydrovalve/Application.java | 22 + .../hydrovalve/analysis/ModBusAnalysis.java | 23 + .../hydrovalve/analysis/ModBusConstants.java | 34 ++ .../hydrovalve/analysis/ModBusEntity.java | 42 ++ .../hydrovalve/analysis/ModBusError.java | 50 ++ .../analysis/ModBusRtuAnalysis.java | 171 ++++++ .../plugins/hydrovalve/conf/BeanConfig.java | 37 ++ .../hydrovalve/conf/HydrovalveConfig.java | 21 + .../hydrovalve/service/FakeThingService.java | 46 ++ .../hydrovalve/service/ModBusDevice.java | 62 +++ .../hydrovalve/service/ModbusPlugin.java | 99 ++++ .../hydrovalve/service/ModbusVerticle.java | 182 +++++++ .../plugins/hydrovalve/utils/ByteUtils.java | 58 ++ .../src/main/resources/application.yml | 8 + .../src/main/resources/config.json | 23 + modbus-plugin/pom.xml | 74 +++ .../cc/iotkit/plugins/modbus/Application.java | 21 + .../plugins/modbus/conf/BeanConfig.java | 36 ++ .../modbus/service/FakeThingService.java | 47 ++ .../plugins/modbus/service/ModbusPlugin.java | 132 +++++ .../src/main/resources/application.yml | 5 + modbus-plugin/src/main/resources/script.js | 24 + mqtt-plugin/pom.xml | 85 +++ .../cc/iotkit/plugins/mqtt/Application.java | 19 + .../iotkit/plugins/mqtt/conf/BeanConfig.java | 28 + .../iotkit/plugins/mqtt/conf/MqttConfig.java | 31 ++ .../mqtt/service/FakeThingService.java | 70 +++ .../plugins/mqtt/service/MqttDevice.java | 88 +++ .../plugins/mqtt/service/MqttPlugin.java | 94 ++++ .../plugins/mqtt/service/MqttVerticle.java | 443 +++++++++++++++ .../mqtt/service/MyChannelInitializer.java | 14 + .../src/main/resources/application.yml | 6 + mqtt-plugin/src/main/resources/config.json | 9 + mqtt-test-client/pom.xml | 87 +++ .../java/cc/iotkit/test/mqtt/Application.java | 10 + .../iotkit/test/mqtt/config/BeanConfig.java | 24 + .../iotkit/test/mqtt/config/MqttConfig.java | 26 + .../cc/iotkit/test/mqtt/model/Request.java | 26 + .../cc/iotkit/test/mqtt/model/Response.java | 31 ++ .../test/mqtt/performance/ConnectionTest.java | 69 +++ .../test/mqtt/performance/ReportTest.java | 88 +++ .../cc/iotkit/test/mqtt/service/Device.java | 28 + .../cc/iotkit/test/mqtt/service/Gateway.java | 164 ++++++ .../test/mqtt/service/MessageHandler.java | 100 ++++ .../iotkit/test/mqtt/service/ReportTask.java | 67 +++ .../cc/iotkit/test/mqtt/service/Vertxs.java | 21 + .../src/main/resources/application.yml | 8 + pom.xml | 96 ++++ tcp-plugin/pom.xml | 87 +++ .../cc/iotkit/plugins/tcp/Application.java | 21 + .../plugins/tcp/cilent/VertxTcpClient.java | 95 ++++ .../iotkit/plugins/tcp/conf/BeanConfig.java | 38 ++ .../plugins/tcp/conf/FakeThingService.java | 47 ++ .../plugins/tcp/conf/TcpServerConfig.java | 34 ++ .../plugins/tcp/parser/DataDecoder.java | 23 + .../plugins/tcp/parser/DataEncoder.java | 24 + .../plugins/tcp/parser/DataPackage.java | 61 +++ .../iotkit/plugins/tcp/parser/DataReader.java | 44 ++ .../iotkit/plugins/tcp/server/TcpDevice.java | 88 +++ .../iotkit/plugins/tcp/server/TcpPlugin.java | 97 ++++ .../plugins/tcp/server/TcpServerVerticle.java | 266 +++++++++ tcp-plugin/src/main/resources/application.yml | 7 + tcp-plugin/src/main/resources/config.json | 16 + tcp-plugin/src/main/resources/script.js | 33 ++ .../test/java/cc/iotkit/test/ScriptTest.java | 26 + .../java/cc/iotkit/test/TcpClientTest.java | 21 + .../cc/iotkit/test/TcpClientVerticle.java | 132 +++++ tcp-plugin/src/test/resources/script.js | 33 ++ websocket-plugin/pom.xml | 74 +++ .../iotkit/plugins/websocket/Application.java | 19 + .../websocket/analysis/DataAnalysis.java | 38 ++ .../iotkit/plugins/websocket/beans/Event.java | 34 ++ .../plugins/websocket/conf/BeanConfig.java | 28 + .../websocket/conf/WebsocketConfig.java | 33 ++ .../websocket/service/FakeThingService.java | 53 ++ .../websocket/service/WebsocketDevice.java | 85 +++ .../websocket/service/WebsocketPlugin.java | 89 +++ .../websocket/service/WebsocketVerticle.java | 301 +++++++++++ .../src/main/resources/application.yml | 8 + .../src/main/resources/config.json | 23 + 144 files changed, 10985 insertions(+) create mode 100644 .gitee/ISSUE_TEMPLATE.zh-CN.md create mode 100644 .gitee/PULL_REQUEST_TEMPLATE.zh-CN.md create mode 100644 .gitignore create mode 100644 DLT645-plugin/pom.xml create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/Application.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Analysis.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Converter.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645Data.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645DataFormat.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645FunCode.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645V1997Data.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/analysis/DLT645V2007Data.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/conf/BeanConfig.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/conf/TcpClientConfig.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/constants/DLT645Constant.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/load/DLT645v1997CsvLoader.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/load/DLT645v2007CsvLoader.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/DLT645Device.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/Dlt645Plugin.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/FakeThingService.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/service/TcpClientVerticle.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ByteRef.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ByteUtils.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/BytesRef.java create mode 100644 DLT645-plugin/src/main/java/cc/iotkit/plugins/dlt645/utils/ContainerUtils.java create mode 100644 DLT645-plugin/src/main/resources/DLT645-1997.csv create mode 100644 DLT645-plugin/src/main/resources/DLT645-2007.csv create mode 100644 DLT645-plugin/src/main/resources/application.yml create mode 100644 DLT645-plugin/src/main/resources/config.json create mode 100644 LICENSE create mode 100644 README.en.md create mode 100644 README.md create mode 100644 emqx-plugin/pom.xml create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/Application.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/BCDClockGenerator.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/BeanConfig.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/CRC16ModbusUtil.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/DeviceStatusParser.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/EventTypeMapper.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/GasAlarmDataParser.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/IoTConfigProtocol.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/MqttConfig.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ProtocolTest.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ResponseParserDemo.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/handler/IMsgHandler.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/AuthVerticle.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/EmqxPlugin.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/FakeThingService.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/service/MqttDevice.java create mode 100644 emqx-plugin/src/main/resources/application.yml create mode 100644 emqx-plugin/src/main/resources/config.json create mode 100644 http-plugin/pom.xml create mode 100644 http-plugin/src/main/java/cc/iotkit/plugins/http/Application.java create mode 100644 http-plugin/src/main/java/cc/iotkit/plugins/http/conf/BeanConfig.java create mode 100644 http-plugin/src/main/java/cc/iotkit/plugins/http/conf/HttpConfig.java create mode 100644 http-plugin/src/main/java/cc/iotkit/plugins/http/service/FakeThingService.java create mode 100644 http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpDevice.java create mode 100644 http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpPlugin.java create mode 100644 http-plugin/src/main/java/cc/iotkit/plugins/http/service/HttpVerticle.java create mode 100644 http-plugin/src/main/resources/application.yml create mode 100644 http-plugin/src/main/resources/config.json create mode 100644 http-plugin/src/main/resources/script.js create mode 100644 http-plugin/src/test/java/cc/iotkit/test/http/HttpTest.java create mode 100644 hydrovalve-plugin/pom.xml create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/Application.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusAnalysis.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusConstants.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusEntity.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusError.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/analysis/ModBusRtuAnalysis.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/conf/BeanConfig.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/conf/HydrovalveConfig.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/FakeThingService.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModBusDevice.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModbusPlugin.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/service/ModbusVerticle.java create mode 100644 hydrovalve-plugin/src/main/java/cc/iotkit/plugins/hydrovalve/utils/ByteUtils.java create mode 100644 hydrovalve-plugin/src/main/resources/application.yml create mode 100644 hydrovalve-plugin/src/main/resources/config.json create mode 100644 modbus-plugin/pom.xml create mode 100644 modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/Application.java create mode 100644 modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/conf/BeanConfig.java create mode 100644 modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/service/FakeThingService.java create mode 100644 modbus-plugin/src/main/java/cc/iotkit/plugins/modbus/service/ModbusPlugin.java create mode 100644 modbus-plugin/src/main/resources/application.yml create mode 100644 modbus-plugin/src/main/resources/script.js create mode 100644 mqtt-plugin/pom.xml create mode 100644 mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/Application.java create mode 100644 mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/conf/BeanConfig.java create mode 100644 mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/conf/MqttConfig.java create mode 100644 mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/FakeThingService.java create mode 100644 mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttDevice.java create mode 100644 mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttPlugin.java create mode 100644 mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MqttVerticle.java create mode 100644 mqtt-plugin/src/main/java/cc/iotkit/plugins/mqtt/service/MyChannelInitializer.java create mode 100644 mqtt-plugin/src/main/resources/application.yml create mode 100644 mqtt-plugin/src/main/resources/config.json create mode 100644 mqtt-test-client/pom.xml create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/Application.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/config/BeanConfig.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/config/MqttConfig.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/model/Request.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/model/Response.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/performance/ConnectionTest.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/performance/ReportTest.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Device.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Gateway.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/MessageHandler.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/ReportTask.java create mode 100644 mqtt-test-client/src/main/java/cc/iotkit/test/mqtt/service/Vertxs.java create mode 100644 mqtt-test-client/src/main/resources/application.yml create mode 100644 pom.xml create mode 100644 tcp-plugin/pom.xml create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/Application.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/cilent/VertxTcpClient.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/BeanConfig.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/FakeThingService.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/conf/TcpServerConfig.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataDecoder.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataEncoder.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataPackage.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/parser/DataReader.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpDevice.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpPlugin.java create mode 100644 tcp-plugin/src/main/java/cc/iotkit/plugins/tcp/server/TcpServerVerticle.java create mode 100644 tcp-plugin/src/main/resources/application.yml create mode 100644 tcp-plugin/src/main/resources/config.json create mode 100644 tcp-plugin/src/main/resources/script.js create mode 100644 tcp-plugin/src/test/java/cc/iotkit/test/ScriptTest.java create mode 100644 tcp-plugin/src/test/java/cc/iotkit/test/TcpClientTest.java create mode 100644 tcp-plugin/src/test/java/cc/iotkit/test/TcpClientVerticle.java create mode 100644 tcp-plugin/src/test/resources/script.js create mode 100644 websocket-plugin/pom.xml create mode 100644 websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/Application.java create mode 100644 websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/analysis/DataAnalysis.java create mode 100644 websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/beans/Event.java create mode 100644 websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/conf/BeanConfig.java create mode 100644 websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/conf/WebsocketConfig.java create mode 100644 websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/FakeThingService.java create mode 100644 websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketDevice.java create mode 100644 websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketPlugin.java create mode 100644 websocket-plugin/src/main/java/cc/iotkit/plugins/websocket/service/WebsocketVerticle.java create mode 100644 websocket-plugin/src/main/resources/application.yml create mode 100644 websocket-plugin/src/main/resources/config.json 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 From c696dc477d33f2d650537ea72df0878c4e1251d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=B1=BC=E5=B9=B2?= <1810377322@163.com> Date: Mon, 7 Jul 2025 18:13:37 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E7=87=83=E6=B0=94=E6=8A=A5=E8=AD=A6?= =?UTF-8?q?=E5=99=A8=E8=A7=A3=E6=9E=90\=E5=8A=A8=E7=81=AB=E7=A6=BB?= =?UTF-8?q?=E4=BA=BA=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- emqx-plugin/pom.xml | 2 +- .../plugins/emqx/conf/CRC16ModbusUtil.java | 35 ++- .../plugins/emqx/conf/EventTypeMapper.java | 39 +-- .../emqx/conf/FireSafetyDataParser.java | 110 ++++++++ .../emqx/conf/FireSafetyDataReceiver.java | 187 ++++++++++++++ .../plugins/emqx/conf/GasAlarmDataParser.java | 163 +++++++++--- .../plugins/emqx/conf/GasDetectorParser.java | 44 ++++ .../plugins/emqx/conf/IoTConfigProtocol.java | 111 ++++++-- .../plugins/emqx/conf/MqttDataProcessor.java | 105 ++++++++ .../plugins/emqx/conf/MqttHexProcessor.java | 71 ++++++ .../plugins/emqx/conf/ProtocolTest.java | 180 ++++++++++++- .../plugins/emqx/service/EmqxPlugin.java | 228 ++++++++++++++--- .../plugins/emqx/service/MqttDevice.java | 240 ++++++++++++++---- pom.xml | 2 +- 14 files changed, 1337 insertions(+), 180 deletions(-) create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/FireSafetyDataParser.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/FireSafetyDataReceiver.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/GasDetectorParser.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/MqttDataProcessor.java create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/MqttHexProcessor.java diff --git a/emqx-plugin/pom.xml b/emqx-plugin/pom.xml index 5f5b31a..54ad1bc 100644 --- a/emqx-plugin/pom.xml +++ b/emqx-plugin/pom.xml @@ -5,7 +5,7 @@ iot-iita-plugins cc.iotkit.plugins - 2.10.19 + 2.10.18 4.0.0 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 index cbb875c..46691e3 100644 --- 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 @@ -6,10 +6,21 @@ public class CRC16ModbusUtil { public static String calculate(String hexStr) { byte[] data = hexToByteArray(hexStr); - int crc = calculateCrc(data); + int crc = calculateCrcWithSwap(data); return String.format("%04X", crc); } - + private static int calculateCrcWithSwap(byte[] data) { + int crc = 0xFFFF; // INIT_VALUE应为0xFFFF + for (byte b : data) { + crc ^= (b & 0xFF); + for (int i = 0; i < 8; i++) { + boolean lsb = (crc & 1) == 1; + crc >>>= 1; + if (lsb) crc ^= 0xA001; // POLY应为0xA001 + } + } + return ((crc & 0xFF) << 8) | ((crc >> 8) & 0xFF); // 关键交换逻辑 + } private static int calculateCrc(byte[] data) { int crc = INIT_VALUE; for (byte b : data) { @@ -53,12 +64,24 @@ public class CRC16ModbusUtil { 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; + //额外加上长度的两个字节 + Integer length =headerLength.length()/2 + 2; + String lengthHex = String.format("%04X", length); + String header = "AA" + type + version + lengthHex + mid + cmd; + // 转换为2字节16进制(大端序) + System.out.println("dataHex+++++++++++++++++++++++++++++++" + dataHex); + System.out.println("header+++++++++++++++++++++++++++++++" + header); + String crcData = lengthHex + mid + cmd + dataHex; + System.out.println("crcData+++++++++++++++++++++++++++++++" + crcData); String crc = calculate(crcData); return header + dataHex + crc + "55"; } - + public static byte[] toBigEndianBytes(int length) { + if(length > 65535) throw new IllegalArgumentException("长度超过2字节范围"); + return new byte[] { + (byte)((length >> 8) & 0xFF), // 高字节 + (byte)(length & 0xFF) // 低字节 + }; + } } 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 index 6bb2980..397c573 100644 --- 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 @@ -18,28 +18,31 @@ public class EventTypeMapper { 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(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(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(30, "自检"); + EVENT_TYPE_MAP.put(31, "备电故障"); + EVENT_TYPE_MAP.put(32, "主电故障"); + EVENT_TYPE_MAP.put(33, "主电欠压"); + EVENT_TYPE_MAP.put(34, "控制器复位"); + EVENT_TYPE_MAP.put(35, "控制器开机"); + EVENT_TYPE_MAP.put(36, "主电故障恢复"); + EVENT_TYPE_MAP.put(37, "备电故障恢复"); + EVENT_TYPE_MAP.put(38, "主电欠压恢复"); // 动火离人相关事件 - 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(50, "设备开机"); + EVENT_TYPE_MAP.put(51, "动火离人报警"); + EVENT_TYPE_MAP.put(52, "动火离人报警恢复"); + EVENT_TYPE_MAP.put(53, "设备故障"); + EVENT_TYPE_MAP.put(54, "设备故障恢复"); + EVENT_TYPE_MAP.put(55, "断网故障"); + EVENT_TYPE_MAP.put(56, "断网故障恢复"); // 正常上报事件 EVENT_TYPE_MAP.put(128, "燃气报警器正常数据上报"); diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/FireSafetyDataParser.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/FireSafetyDataParser.java new file mode 100644 index 0000000..245a737 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/FireSafetyDataParser.java @@ -0,0 +1,110 @@ +package cc.iotkit.plugins.emqx.conf; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +public class FireSafetyDataParser { + // 事件类型常量 + public static final int EVENT_POWER_ON = 50; + public static final int EVENT_FIRE_ALARM = 51; + public static final int EVENT_ALARM_RESET = 52; + public static final int EVENT_DEVICE_FAIL = 53; + public static final int EVENT_FAIL_RESET = 54; + public static final int EVENT_NET_DOWN = 55; + public static final int EVENT_NET_RECOVER = 56; + + // 数据项ID常量 + public static final int ITEM_TEMP_DISTANCE = 0x0040; + public static final int ITEM_ALARM_DURATION = 0x0041; + public static final int ITEM_SOUP_ALARM = 0x0042; + public static final int ITEM_VOLUME = 0x0043; + public static final int ITEM_SOUP_MODE = 0x0044; + public static final int ITEM_ALARM_STATUS = 0x0045; + public static final int ITEM_FLAME_DETECT = 0x0046; + public static final int ITEM_HUMAN_DETECT = 0x0047; + public static final int ITEM_ERROR_CODE = 0x0048; + + public static Map parseData(byte[] rawData) { + ByteBuffer buffer = ByteBuffer.wrap(rawData); + Map result = new HashMap<>(); + result.put("dataType", "HOT_WORK_DATA"); + // 解析事件头(1字节事件类型 + 6字节BCD时间) + // int eventType = buffer.get() & 0xFF; + int eventType = buffer.getShort() & 0xFFFF; + // buffer.getShort() & 0xFFFF + result.put("eventType", eventType); + result.put("timestamp", GasAlarmDataParser.parseBcdTime(buffer)); + result.put("eventTypeValue", EventTypeMapper.getEventTypeName(Integer.parseInt(result.get("eventType").toString()))); + // 解析数据域 + if (buffer.remaining() > 0) { + int dataLength = buffer.get() & 0xFF; + while (buffer.remaining() >= 4) { // 每个数据项至少4字节(2字节ID+2字节数据) + int itemId = buffer.getShort() & 0xFFFF; + int itemValue = buffer.getShort() & 0xFFFF; + result.put(getItemName(itemId), parseItemValue(itemId, itemValue)); + } + } + return result; + } + + /* private static String parseBcdTime(ByteBuffer buffer) { + byte[] bcdTime = new byte[6]; + buffer.get(bcdTime); + return String.format("20%02d-%02d-%02d %02d:%02d:%02d", + bcdTime[0], bcdTime[1], bcdTime[2], bcdTime[3], bcdTime[4], bcdTime[5]); + }*/ + + private static String getItemName(int itemId) { + switch (itemId) { + case ITEM_TEMP_DISTANCE: return "temperatureDistance"; + case ITEM_ALARM_DURATION: return "alarmDuration"; + case ITEM_SOUP_ALARM: return "soupAlarmTime"; + case ITEM_VOLUME: return "volumeLevel"; + case ITEM_SOUP_MODE: return "soupMode"; + case ITEM_ALARM_STATUS: return "alarmStatus"; + case ITEM_FLAME_DETECT: return "flameDetected"; + case ITEM_HUMAN_DETECT: return "humanDetected"; + case ITEM_ERROR_CODE: return "errorCode"; + default: return "unknown_" + Integer.toHexString(itemId); + } + } + + private static Object parseItemValue(int itemId, int value) { + switch (itemId) { + + // 测温距离(0x0040) + case ITEM_TEMP_DISTANCE: + return new String[]{"2米","2-3米","3-3.5米"}[value-1]; + // 动火报警时间(0x0041) + case ITEM_ALARM_DURATION: + return value + "分钟"; + + // 煲汤报警时间(0x0042) + case ITEM_SOUP_ALARM: + return (value > 60) ? + (value/60 + "小时" + value%60 + "分钟") : + (value + "分钟"); + // 音量控制(0x0043) + case ITEM_VOLUME: + return new String[]{"静音","小","中","大"}[value]; + // 煲汤模式(0x0044) + case ITEM_SOUP_MODE: + return value == 0 ? "普通模式" : "煲汤模式"; + // 报警状态(0x0045) + case ITEM_ALARM_STATUS: + return value == 0 ? "未报警" : "报警中"; + // 火苗检测(0x0046) + case ITEM_FLAME_DETECT: + return value == 0 ? "未检测到火苗" : "检测到火苗"; + // 人体检测(0x0047) + case ITEM_HUMAN_DETECT: + return value == 0 ? "无人员" : "有人员"; + // 故障码(预留) + case ITEM_ERROR_CODE: + return String.format("故障码0x%04X", value); + default: + return value; + } + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/FireSafetyDataReceiver.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/FireSafetyDataReceiver.java new file mode 100644 index 0000000..2376a2b --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/FireSafetyDataReceiver.java @@ -0,0 +1,187 @@ +package cc.iotkit.plugins.emqx.conf; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + + +public class FireSafetyDataReceiver { + public static void main(String[] args) { + // 测试16进制报文:17(事件类型) + 250616123456(时间) + 02(数据长度) + 0040(测温距离ID) + 0002(2-3米) + String hexData = "17 25 06 16 12 34 56 02 00 40 00 02"; + Map result = handleHexData(hexData); + + System.out.println("解析结果:" + result); + result.forEach((k, v) -> System.out.println(k + ": " + v)); + } + /*public void processDataStream(InputStream inputStream) throws Exception { + byte[] header = new byte[HEADER_SIZE]; + while (inputStream.read(header) == HEADER_SIZE) { + int dataLength = inputStream.read(); + byte[] payload = new byte[dataLength]; + inputStream.read(payload); + + byte[] fullData = new byte[HEADER_SIZE + 1 + dataLength]; + System.arraycopy(header, 0, fullData, 0, HEADER_SIZE); + fullData[HEADER_SIZE] = (byte)dataLength; + System.arraycopy(payload, 0, fullData, HEADER_SIZE+1, dataLength); + + Map parsedData = FireSafetyDataParser.parseData(fullData); + handleEvent(parsedData); + } + }*/ + public static Map handleHexData(String hexString) { + byte[] data = hexStringToBytes(hexString); + if (data == null || data.length < 1) { + return null; + } + + int eventType = Byte.toUnsignedInt(data[0]) + Byte.toUnsignedInt(data[1]); + if (eventType == 129) { + return handleEvent(data); + } + return OpenFireHandleEvent(data); + } + + private static byte[] hexStringToBytes(String hex) { + hex = hex.replaceAll("\\s", ""); + byte[] bytes = new byte[hex.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + return bytes; + } + /* + * 动火离人告警事件 + * */ + public static Map OpenFireHandleEvent(byte[] data) { + Map result= new HashMap<>(); + ByteBuffer buffer = ByteBuffer.wrap(data); + result.put("dataType", "HOT_WORK_DATA"); + int eventType = buffer.getShort() & 0xFFFF; + // 解析事件头 + //int eventType = buffer.get() & 0xFF; + result.put("eventType", eventType); + result.put("timestamp", GasAlarmDataParser.parseBcdTime(buffer)); + result.put("eventTypeValue", EventTypeMapper.getEventTypeName(Integer.parseInt(result.get("eventType").toString()))); + // 完整事件处理 + /* switch (eventType) { + case FireSafetyDataParser.EVENT_POWER_ON: + result.put("eventName", "设备开机"); + // result.put("action", "记录启动日志"); + break; + case FireSafetyDataParser.EVENT_FIRE_ALARM: + result.put("eventName", "动火离人报警"); + //result.put("action", "触发声光报警+推送告警"); + // parseDataItems(buffer, result); + break; + case FireSafetyDataParser.EVENT_ALARM_RESET: + result.put("eventName", "动火离人报警恢复"); + // result.put("action", "停止报警+记录处置"); + break; + case FireSafetyDataParser.EVENT_DEVICE_FAIL: + result.put("eventName", "设备故障"); + // result.put("action", "启动自检+通知维护"); + // parseDataItems(buffer, result); + break; + case FireSafetyDataParser.EVENT_FAIL_RESET: + result.put("eventName", "设备故障恢复"); + // result.put("action", "更新设备状态"); + break; + case FireSafetyDataParser.EVENT_NET_DOWN: + result.put("eventName", "断网故障"); + // result.put("action", "启用本地存储"); + break; + case FireSafetyDataParser.EVENT_NET_RECOVER: + result.put("eventName", "断网故障恢复"); + // result.put("action", "同步缓存数据"); + break; + default: + result.put("eventName", "未知事件"); + // result.put("action", "需要人工核查"); + }*/ + System.out.println("解析后的数据" + result); + return result; + } + /* + * 动火离人正常上报数据解析 + * */ + public static Map handleEvent(byte[] data) { + // Map result = new HashMap<>(); + Map result= FireSafetyDataParser.parseData(data); + /* ByteBuffer buffer = ByteBuffer.wrap(data); + + // 解析事件头 + int eventType = buffer.get() & 0xFF; + result.put("eventType", eventType); + result.put("timestamp", GasAlarmDataParser.parseBcdTime(buffer)); + + // 完整事件处理 + switch (eventType) { + case FireSafetyDataParser.EVENT_POWER_ON: + result.put("eventName", "设备开机"); + // result.put("action", "记录启动日志"); + break; + case FireSafetyDataParser.EVENT_FIRE_ALARM: + result.put("eventName", "动火离人报警"); + //result.put("action", "触发声光报警+推送告警"); + // parseDataItems(buffer, result); + break; + case FireSafetyDataParser.EVENT_ALARM_RESET: + result.put("eventName", "动火离人报警恢复"); + // result.put("action", "停止报警+记录处置"); + break; + case FireSafetyDataParser.EVENT_DEVICE_FAIL: + result.put("eventName", "设备故障"); + // result.put("action", "启动自检+通知维护"); + // parseDataItems(buffer, result); + break; + case FireSafetyDataParser.EVENT_FAIL_RESET: + result.put("eventName", "设备故障恢复"); + // result.put("action", "更新设备状态"); + break; + case FireSafetyDataParser.EVENT_NET_DOWN: + result.put("eventName", "断网故障"); + // result.put("action", "启用本地存储"); + break; + case FireSafetyDataParser.EVENT_NET_RECOVER: + result.put("eventName", "断网故障恢复"); + // result.put("action", "同步缓存数据"); + break; + default: + result.put("eventName", "未知事件"); + // result.put("action", "需要人工核查"); + }*/ + System.out.println("解析后的数据" + result); + return result; + } + /* 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 parseBcdTime(ByteBuffer buffer) { + byte[] timeBytes = new byte[6]; + buffer.get(timeBytes); + return String.format("20%02d-%02d-%02d %02d:%02d:%02d", + timeBytes[0], timeBytes[1], timeBytes[2], + timeBytes[3], timeBytes[4], timeBytes[5]); + } +*/ + + private static void parseDataItems(ByteBuffer buffer, Map result) { + if (buffer.remaining() > 0) { + int dataLength = buffer.get() & 0xFF; + while (buffer.remaining() >= 4) { + int itemId = buffer.getShort() & 0xFFFF; + int value = buffer.getShort() & 0xFFFF; + result.put("item_" + Integer.toHexString(itemId), value); + } + } + } +} 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 index 6a6a3d8..d0b9210 100644 --- 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 @@ -2,10 +2,20 @@ package cc.iotkit.plugins.emqx.conf; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.chrono.IsoChronology; +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; import java.util.*; public class GasAlarmDataParser { public static void main(String[] args) { + // 测试数据:250707131758 -> 对应字节数组 {0x25, 0x07, 0x07, 0x13, 0x17, 0x58} + byte[] testData = new byte[]{0x25, 0x07, 0x07, 0x13, 0x17, 0x58}; + ByteBuffer buffer = ByteBuffer.wrap(testData); + + String result = parseBcdTime(buffer); + System.out.println("解析结果: " + result); // 节点相关事件测试数据 (1-12) // 节点相关事件测试数据 (1-12) - 数值部分改为低位在前 String[] nodeEvents = { @@ -63,10 +73,10 @@ public class GasAlarmDataParser { System.out.println("\n=== " + groupName + "测试 ==="); for (String hexData : testCases) { try { - List> result = GasAlarmDataParser.parseHexDataToList(hexData); + Map result = GasAlarmDataParser.parseHexDataToList(hexData); System.out.println("原始数据: " + hexData); - System.out.println("解析结果: " + result.get(0).get("eventType") - + " - " + result.get(0).get("dataType")); + System.out.println("解析结果: " + result.get("eventType") + + " - " + result.get("dataType")); System.out.println("rrr解析结果: " + result.get(0)); @@ -75,18 +85,21 @@ public class GasAlarmDataParser { } } } - public static List> parseHexDataToList(String hexData) { + + public static Map parseHexDataToList(String hexData) { + System.out.println("parseHexDataToList: " + hexData); byte[] data = hexStringToByteArray(hexData); return parseDataToList(data); } - public static List> parseDataToList(byte[] data) { - List> resultList = new ArrayList<>(); + public static Map parseDataToList(byte[] data) { + Map resultList = new HashMap<>(); if (data == null || data.length < 1) { return resultList; } int eventType = Byte.toUnsignedInt(data[0]) + Byte.toUnsignedInt(data[1]); + System.out.println("eventType: " + eventType); if (eventType == 128) { // // 正常数据上报 (NORMAL_DATA) //4.2.3 终端上报连接节点数据域格式 @@ -102,8 +115,8 @@ public class GasAlarmDataParser { return parseNodeEventToList(data); } - private static List> parseControllerEventToList(byte[] data) { - List> eventList = new ArrayList<>(); + private static Map parseControllerEventToList(byte[] data) { + // List> eventList = new ArrayList<>(); ByteBuffer buffer = ByteBuffer.wrap(data); Map eventMap = new LinkedHashMap<>(); @@ -111,29 +124,41 @@ public class GasAlarmDataParser { 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); + // eventList.add(eventMap); - return eventList; + return eventMap; } - private static List> parseNodeEventToList(byte[] data) { - List> eventList = new ArrayList<>(); + private static Map parseNodeEventToList(byte[] data) { + // Map eventList = new HashMap<>(); 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); + if (!Objects.equals(eventMap.get("eventType"), 15)) { + 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); + } else { + eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(Integer.parseInt(eventMap.get("eventType").toString()))); + eventMap.put("timestamp", parseBcdTime(buffer)); + eventMap.put("totalNodes", buffer.getShort() & 0xFF); + eventMap.put("nodeId", buffer.getShort() & 0xFFFF); + while (buffer.remaining() >= 2) { + eventMap.put("dataValue", buffer.getShort() & 0xFFFF); + } - return eventList; + } + System.out.println("eventMap+++++++++++: " + eventMap); + //eventList.add(eventMap); + + return eventMap; } - private static List> parseNormalDataToList(byte[] data) { - List> dataList = new ArrayList<>(); + private static Map parseNormalDataToList(byte[] data) { + // List> dataList = new ArrayList<>(); ByteBuffer buffer = ByteBuffer.wrap(data); // 基础信息 @@ -144,32 +169,91 @@ public class GasAlarmDataParser { baseInfo.put("timestamp", parseBcdTime(buffer)); baseInfo.put("totalNodes", buffer.getShort() & 0xFFFF); baseInfo.put("reportedNodes", buffer.get() & 0xFF); - dataList.add(baseInfo); - + // dataList.add(baseInfo); + System.out.println(buffer.remaining()); // 节点数据 - 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")){ + // 节点数据(动态添加数字后缀) + int nodeCounter = 0; + while (buffer.remaining() >= 7) { + String suffix = String.valueOf(nodeCounter++); + // Map nodeMap = new LinkedHashMap<>(); + baseInfo.put("nodeId" + suffix, buffer.getShort() & 0xFFFF); + baseInfo.put("nodeType" + suffix, buffer.get() & 0xFF); + // nodeMap.put("unit", buffer.get() & 0xFF); // 节点节点参数 + // nodeMap.put("precision", buffer.get() & 0xFF); + baseInfo.put("gasType" + suffix, GasDetectorParser.parse(buffer.getShort() & 0xFFFF)); + baseInfo.put("dataValue" + suffix, buffer.getShort() & 0xFFFF); + if (Objects.equals(baseInfo.get("nodeType" + suffix), 0)) { // 0:节点为探测器 - nodeMap.put("dataTypeValue", nodeMap.get("dataValue")); - }else if (nodeMap.get("nodeType").equals("1")){ + baseInfo.put("dataTypeValue" + suffix, baseInfo.get("dataValue" + suffix)); + } else if (Objects.equals(baseInfo.get("nodeType" + suffix), 1)) { //1:节点为输出模块 - nodeMap.put("dataTypeValue",nodeMap.get("dataValue").equals("0")?"未动作":"已动作"); + baseInfo.put("dataTypeValue" + suffix, baseInfo.get("dataValue" + suffix).equals("0") ? "未动作" : "已动作"); } - dataList.add(nodeMap); + // dataList.add(nodeMap); } - return dataList; + return baseInfo; } + // 辅助方法:字节数组转十六进制字符串 + + private static String bytesToHex(byte[] bytes) { + + StringBuilder hexSb = new StringBuilder(); + for (byte b : bytes) { + hexSb.append(String.format("%02X", b)); + } + return hexSb.toString(); + } // 保留原有辅助方法 - private static String parseBcdTime(ByteBuffer buffer) { + /*public static String parseBcdTime(ByteBuffer buffer) { + // 打印ByteBuffer的十六进制表示 + System.out.println("ByteBuffer内容(十六进制): " + bytesToHex(buffer.array())); + + byte[] bcdTime = new byte[6]; + + buffer.get(bcdTime); + + StringBuilder sb = new StringBuilder(); + + for (byte b : bcdTime) { + + // 提取高四位和低四位,并转换为十进制数字 + + int high = (b >> 4) & 0x0F; + + int low = b & 0x0F; + + // 组合成两位十进制字符串 + + sb.append(String.format("%1d%1d", high, low)); + + } + + String bcdStr = sb.toString(); + + try { + + SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmss"); + + sdf.set2DigitYearStart(new Date(946684800000L)); // 2000-01-01 + + Date date = sdf.parse(bcdStr); + + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); + + } catch (Exception e) { + + return bcdStr; // 返回原始BCD字符串(可能包含越界值) + + } + }*/ + + public static String parseBcdTime(ByteBuffer buffer) { + // 打印ByteBuffer的十六进制表示 + System.out.println("ByteBuffer内容(十六进制): " + bytesToHex(buffer.array())); + byte[] bcdTime = new byte[6]; buffer.get(bcdTime); StringBuilder sb = new StringBuilder(); @@ -178,6 +262,7 @@ public class GasAlarmDataParser { } try { SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmss"); + sdf.set2DigitYearStart(new SimpleDateFormat("yyyy").parse("2000")); // 设置世纪转折点 Date date = sdf.parse(sb.toString()); return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); } catch (Exception e) { @@ -190,12 +275,12 @@ public class GasAlarmDataParser { 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)); + + Character.digit(s.charAt(i + 1), 16)); } return data; } private static boolean isControllerEvent(int eventType) { - return eventType >= 13 && eventType <= 21; + return eventType >= 30 && eventType <= 38; } } diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/GasDetectorParser.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/GasDetectorParser.java new file mode 100644 index 0000000..ea2172d --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/GasDetectorParser.java @@ -0,0 +1,44 @@ +package cc.iotkit.plugins.emqx.conf; + +import java.util.Map; + +public class GasDetectorParser { + + // 探测器精度映射 + private static final Map PRECISION_MAP = Map.of( + 0b000, "*1", + 0b001, "*0.1", + 0b010, "*0.01", + 0b011, "*0.001", + 0b100, "*0.0001" + ); + + // 浓度单位映射 + private static final Map UNIT_MAP = Map.of( + 0b000, "%LEL", + 0b001, "PPM", + 0b010, "%VOL" + ); + + // 气体类型映射 + private static final Map GAS_TYPE_MAP = Map.of( + 0b0000, "甲烷", + 0b0001, "一氧化碳", + 0b0010, "丙烷", + 0b0011, "氢气", + 0b0100, "苯", + 0b0101, "氧气" + ); + + public static String parse(int data) { + int precisionBits = (data >> 13) & 0b111; // 提取bit15-13 + int unitBits = (data >> 10) & 0b111; // 提取bit12-10 + int gasTypeBits = (data >> 6) & 0b1111; // 提取bit9-6 + + return String.format("探测器精度: %s, 浓度单位: %s, 测量气体: %s", + PRECISION_MAP.getOrDefault(precisionBits, "未知精度"), + UNIT_MAP.getOrDefault(unitBits, "未知单位"), + GAS_TYPE_MAP.getOrDefault(gasTypeBits, "未知气体") + ); + } +} 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 index 85ad94c..507d86b 100644 --- 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 @@ -26,21 +26,48 @@ public class IoTConfigProtocol { 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); + // 反向映射Map + private static final Map CONFIG_ID_TO_KEY = new HashMap<>(); + + static { + // 初始化反向映射 + CONFIG_ID_TO_KEY.put(IP_REALM, "ip"); + CONFIG_ID_TO_KEY.put(PORT, "port"); + CONFIG_ID_TO_KEY.put(REPORT_AWAIT_TIME, "reportAwaitTime"); + CONFIG_ID_TO_KEY.put(REPORT_RETRY_TIMES, "reportRetryTimes"); + CONFIG_ID_TO_KEY.put(CARD, "card"); + CONFIG_ID_TO_KEY.put(USERNAME, "username"); + CONFIG_ID_TO_KEY.put(PASSWORD, "password"); + CONFIG_ID_TO_KEY.put(KEEP_ALIVE, "keepAlive"); + CONFIG_ID_TO_KEY.put(PUBLISH_TOPIC, "publishTopicPrefix"); + CONFIG_ID_TO_KEY.put(SUBSCRIBE_TOPIC, "subscribeTopicPrefix"); + CONFIG_ID_TO_KEY.put(REPORT_RATE_UPPER, "reportRateUpper"); + CONFIG_ID_TO_KEY.put(REPORT_CONCENTRATION_RANGE, "reportConcentrationRange"); + CONFIG_ID_TO_KEY.put(REPORT_RATE_LOWER, "reportRateLower"); + CONFIG_ID_TO_KEY.put(SIGNAL, "signal"); + CONFIG_ID_TO_KEY.put(TIME_SYNC, "timeSyncInternal"); + CONFIG_ID_TO_KEY.put(VERSION, "version"); + } + + public static String getKeyByConfigId(int configId) { + return CONFIG_ID_TO_KEY.get(configId); + } + + public static String generateReadCommand( int configId) { + ByteBuffer buf = ByteBuffer.allocate(2); // buf.putShort((short) 0xAA23); // 起始符+命令码 // buf.putShort((short) 4); // 数据长度 - buf.putShort((short) terminalType); + // buf.putShort((short) terminalType); buf.putShort((short) configId); return bytesToHex(buf.array()); } // 生成写入指令 - public static String generateWriteCommand(int terminalType, int configId, byte[] data) { + public static String generateWriteCommand(int configId, byte[] data) { - ByteBuffer buf = ByteBuffer.allocate(5 + data.length); + ByteBuffer buf = ByteBuffer.allocate(3 + data.length); //buf.putShort((short) (5 + data.length)); // 数据长度 - buf.putShort((short) terminalType); + // buf.putShort((short) terminalType); buf.putShort((short) configId); buf.put((byte) data.length); // 数据长度字节 buf.put(data); // 实际数据 @@ -62,6 +89,7 @@ public class IoTConfigProtocol { // 根据配置ID进行类型化解析 switch((int)result.get("configId")) { + case IP_REALM: case PUBLISH_TOPIC: case SUBSCRIBE_TOPIC: @@ -97,21 +125,38 @@ public class IoTConfigProtocol { default: result.put("value", bytesToHex(value)); } + // CONFIG_ID_TO_KEY.get((int)result.get("configId")); + result.put("key", getKeyByConfigId((int)result.get("configId"))); 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); + int maxAvailable = data.length - 1; // 可用最大长度 + if (maxAvailable <= 0 || len <= 0) { + return ""; // 或抛出自定义异常 + } + return new String(data, 1, Math.min(len, maxAvailable), StandardCharsets.US_ASCII); + }*/ +private static String parseTLVString(byte[] data) { + int maxAvailable = data.length; // 可用最大长度 + if (maxAvailable <= 0) { + return ""; // 或抛出自定义异常 } - + return new String(data, 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); + // int len = data[0] & 0xFF; + int maxAvailable = data.length; // 可用最大长度 + if (maxAvailable <= 0) { + return ""; // 或抛出自定义异常 + } + return new String(data, StandardCharsets.US_ASCII); + } - private static String parseBcdTime(byte[] data) { + public 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])); @@ -144,6 +189,30 @@ public class IoTConfigProtocol { System.arraycopy(value, 0, data, 1, value.length); return data; } + /* + * ip地址转换成TLV数据 + * */ + public static byte[] convertToFixedAscii(String input, int fixedLength) { + // 验证输入格式 + if (!isValidIpOrDomain(input)) { + throw new IllegalArgumentException("Invalid IP/Domain format"); + } + + byte[] originBytes = input.getBytes(StandardCharsets.US_ASCII); + byte[] result = new byte[fixedLength]; + + // 复制原始数据并补0 + System.arraycopy(originBytes, 0, result, 0, Math.min(originBytes.length, fixedLength)); + return result; + } + private static boolean isValidIpOrDomain(String input) { + // IP地址正则验证 (IPv4) + String ipPattern = "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$"; + // 域名正则验证 (支持国际化域名IDN的ASCII形式) + String domainPattern = "^((?!-)[A-Za-z0-9-]{1,63}(?>> 1) ^ 0xA001; + } else { + crc >>>= 1; + } + } + } + return crc; + } + + +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/MqttHexProcessor.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/MqttHexProcessor.java new file mode 100644 index 0000000..23e690d --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/MqttHexProcessor.java @@ -0,0 +1,71 @@ +package cc.iotkit.plugins.emqx.conf; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +public class MqttHexProcessor { + + + // 检测是否为HEX字符串格式 + private static boolean isHexString(byte[] data) { + if (data.length % 2 != 0) return false; + for (byte b : data) { + char c = (char) b; + if (!(Character.isDigit(c) || + (c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f') || + Character.isWhitespace(c))) { // 新增空格检查 + return false; + } + } + return true; + } + + // HEX字符串转字节数组 + /* private static byte[] parseHexString(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; + }*/ + + // 统一返回byte[]的入口方法 + public static byte[] processToBytes(byte[] payload) { + try { + String payloadStr = new String(payload, StandardCharsets.UTF_8); + System.out.println("payloadStr:" + payloadStr); + System.out.println("payloadStr1:" + isHexString(payload)); + return isHexString(payload) ? + Hex.decodeHex(payloadStr.replaceAll("\\s+", "")) : + payload; // 原始字节直接返回 + } catch (Exception e) { + return payload; // 异常时返回原始数据 + } + } + + + + /*public static void main(String[] args) throws DecoderException { + String input = "AA 01 01 00 35001100013836323537313037393339313237393839383631313235323037303833363037383837000000000111b12255"; // 原始字符串 + // byte[] result = convertHexStringToBytes(input); + byte[] bytes = Hex.decodeHex(input); + // 基础校验 + + + // 提取各字段 + ByteBuffer buffer = ByteBuffer.wrap(bytes); + byte type = buffer.get(1); + byte version = buffer.get(2); + System.out.println(String.format("%02X", bytes[0])); + System.out.println(type); + System.out.println(version); + + }*/ +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ProtocolTest.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ProtocolTest.java index 07d190f..7340762 100644 --- a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ProtocolTest.java +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ProtocolTest.java @@ -1,14 +1,185 @@ package cc.iotkit.plugins.emqx.conf; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonObject; + +import java.net.URI; +import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.HashMap; import java.util.Map; -import static cc.iotkit.plugins.emqx.conf.IoTConfigProtocol.buildTLVData; +import static cc.iotkit.plugins.emqx.conf.CRC16ModbusUtil.buildFrame; +import static cc.iotkit.plugins.emqx.conf.IoTConfigProtocol.*; +import static cc.iotkit.plugins.emqx.conf.MqttDataProcessor.parseFrame; public class ProtocolTest { + public static ByteBuffer hexStringToByteBuffer(String hex) { + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i+1), 16)); + } + return ByteBuffer.wrap(data); + } + public static byte[] parseFrame1(String hexString) { + if (hexString == null || hexString.length() % 2 != 0) { + throw new IllegalArgumentException("Invalid hex string"); + } + + byte[] bytes = new byte[hexString.length() / 2]; + for (int i = 0; i < hexString.length(); i += 2) { + String byteStr = hexString.substring(i, i + 2); + bytes[i / 2] = (byte) Integer.parseInt(byteStr, 16); + } + return bytes; + } + public static String asciiToHex(String asciiStr) { + StringBuilder hex = new StringBuilder(); + for (char ch : asciiStr.toCharArray()) { + hex.append(String.format("%02X ", (int) ch)); + } + return hex.toString().trim(); + } + public static String hexToAscii(String hexStr) { + StringBuilder output = new StringBuilder(); + // 移除所有空格 + hexStr = hexStr.replaceAll("\\s", ""); + + for (int i = 0; i < hexStr.length(); i += 2) { + String str = hexStr.substring(i, i + 2); + output.append((char) Integer.parseInt(str, 16)); + } + return output.toString(); + } + public static ByteBuffer createBcdTimeBuffer(String bcdTimeStr) { + // 验证输入字符串长度是否为12位(6字节BCD码) + if(bcdTimeStr == null || bcdTimeStr.length() != 12) { + throw new IllegalArgumentException("BCD时间字符串必须是12位数字"); + } + + byte[] bytes = new byte[6]; + for(int i = 0; i < 6; i++) { + // 每两位数字转换为一个BCD字节 + String twoDigits = bcdTimeStr.substring(i*2, i*2+2); + bytes[i] = (byte)Integer.parseInt(twoDigits); + } + + return ByteBuffer.wrap(bytes); + } + public static void main(String[] args) { + + // GasAlarmDataParser.parseBcdTime(createBcdTimeBuffer("250707131758")); + System.out.println(GasAlarmDataParser.parseBcdTime(createBcdTimeBuffer("250703103146"))); + //String url = "https://minio.dddd.com/feijialuo/20250702/lQLPKHYULtpjo8HM_Mz8sCPMoZZYPmEbCD1keBQpVgA_252_2521751416904105.png"; + try { + String url = "https://minio.dddd.com/feijialuo/20250702/lQLPKHYULtpjo8HM_Mz8sCPMoZZYPmEbCD1keBQpVgA_252_2521751416904105.png"; + + // 使用lastIndexOf和substring方法截取URL + int startIndex = url.lastIndexOf("/feijialuo/") + "/feijialuo/".length(); + String result = url.substring(startIndex); + System.out.println("Extracted File Name: " + result); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static String extractDateAndFileNameFromURL(String url) throws URISyntaxException { + // 解析URL并提取路径部分 + URI uri = new URI(url); + String path = uri.getPath(); + + // 分割路径为部分数组 + String[] parts = path.split("/"); + + // 遍历路径段,找到日期和文件名 + String date = null; + String fileName = null; + for (String part : parts) { + if (part.matches("\\d{8}")) { // 检查是否为8位数字日期格式 + date = part; + } else if (fileName == null && !part.isEmpty()) { // 找到第一个非空且非日期的部分作为文件名 + fileName = part; + // 由于我们已经找到了文件名,可以跳出循环(假设文件名之后没有其他重要信息) + // 但为了保持代码的通用性,这里不强制跳出循环 + } + } + + // 拼接日期和文件名 + if (date != null && fileName != null) { + return date + "/" + fileName; + } else { + throw new IllegalArgumentException("Invalid URL format: Unable to extract date and file name."); + } + } + /* public static void main(String[] args) { + String mid = "0x-102"; + String cmd= "0x-103"; + // 获取 mid 的最后两位字符 + if (mid.length() >= 2) { + mid = mid.substring(mid.length() - 2); + } else { + // 如果 mid 长度小于 2,可以根据需求设置默认值或进行其他处理 + // 例如,这里我们设置为空字符串,但实际应用中可能需要更合适的默认值或错误处理 + mid = ""; + } + System.out.println(mid); +// 获取 cmd 的最后两位字符 + if (cmd.length() >= 2) { + cmd = cmd.substring(cmd.length() - 2); + } else { + // 如果 cmd 长度小于 2,同样根据需求设置默认值或进行其他处理 + // 例如,这里我们设置为空字符串 + cmd = ""; + } + System.out.println(cmd); + System.out.println(asciiToHex("4141303130313030313330303234303032353036323530363235313034343539353434453535")); + System.out.println(hexToAscii("4141 3031 3031 3030 3133 3030 3234 3030 3235 3036 3235 3036 3235 3131 3035 3233 4234 3344 3535")); + byte[] bcdTime = buildBcdTimeData("2025-06-25 10:36:05"); + System.out.println(parseBcdTime(buildBcdTimeData("2025-06-25 10:36:05"))); + // 转换为16进制字符串输出 + System.out.print("16进制BCD时间: "); + for (byte b : bcdTime) { + System.out.print(String.format("%02X ", b)); + } + + Map original = new HashMap(); + original.put("test","tset"); + Map map = original; + map.put("test", "value"); + System.out.println(original.get("test")); // 如果输出value说明会影响 + + String replyMessage = buildFrame("01","01", + "00", "24", "0001002506250624154606"); + System.out.println("ddd" + replyMessage); + JsonObject payload = parseFrame(parseFrame1("aa01010023042300000415153839383631313235323037303833363037383837b31055")); + String hexData = "00002615153839383631313235323037303833363037383837"; + String data =payload.getString("data").replaceAll("\\s+", ""); ; + System.out.println(data); + IoTConfigProtocol.parseReadResponse(hexData); + + // 计算字节长度(每2字符=1字节) + int byteLength = hexData.length() / 2; + + // 转换为2字节16进制(大端序) + String lengthHex = String.format("%04X", byteLength); + + System.out.println("原始数据: " + lengthHex); + System.out.println("字节长度: " + byteLength + " (0x" + lengthHex + ")"); + System.out.println("2字节长度值: " + lengthHex.substring(0, 2) + " "+ lengthHex.substring(2)); + + // System.out.println("Buffer内容(HEX): " + buffer.toString("HEX")); + }*/ + + /*public static void main(String[] args) { + // 3. SIM卡号读取 (只读) + String readCardCmd = IoTConfigProtocol.generateReadCommand(0x0001, + IoTConfigProtocol.CARD); + System.out.println("读取SIM卡指令: " + readCardCmd); // 1. IP/域名配置 (TLV格式) String ip = "192.168.1.100"; byte[] ipData = buildTLVData(ip.getBytes()); @@ -23,10 +194,7 @@ public class ProtocolTest { IoTConfigProtocol.PORT, portData); System.out.println("写入端口指令: " + writePortCmd); - // 3. SIM卡号读取 (只读) - String readCardCmd = IoTConfigProtocol.generateReadCommand(0x01, - IoTConfigProtocol.CARD); - System.out.println("读取SIM卡指令: " + readCardCmd); + // 4. 时间同步设置 (BCD编码) byte[] timeData = new byte[6]; @@ -49,7 +217,7 @@ public class ProtocolTest { String readSignalCmd = IoTConfigProtocol.generateReadCommand(0x01, IoTConfigProtocol.SIGNAL); System.out.println("读取信号指令: " + readSignalCmd); - } + }*/ // 构建TLV格式数据 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 index 861048b..e4d0e28 100644 --- 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 @@ -6,18 +6,15 @@ 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.*; +import cc.iotkit.plugin.core.thing.actions.down.DeviceConfig; 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 cc.iotkit.plugins.emqx.conf.*; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; import com.gitee.starblues.bootstrap.annotation.AutowiredType; import com.gitee.starblues.bootstrap.realize.PluginCloseListener; import com.gitee.starblues.core.PluginCloseType; @@ -25,6 +22,7 @@ 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.buffer.Buffer; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.mqtt.MqttClient; @@ -36,6 +34,7 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @@ -45,6 +44,9 @@ 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; +import static cc.iotkit.plugins.emqx.conf.MqttDataProcessor.parseFrame; +import static cc.iotkit.plugins.emqx.conf.MqttHexProcessor.processToBytes; + /** * @author sjg @@ -97,8 +99,8 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { String serverPassword = IdUtil.fastSimpleUUID(); MqttClientOptions options = new MqttClientOptions() - .setClientId("server") - .setUsername("server") + .setClientId("server11223") + .setUsername("server11223") .setPassword(serverPassword) .setCleanSession(true) .setMaxInflightQueue(100) @@ -175,16 +177,21 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { }).publishHandler(msg -> { String topic = msg.topicName(); log.info("topic={}",topic); - if (topic.contains("/c/")) { + if (topic.contains("/request/")) { 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()); + if (topic != null && topic.startsWith("FIGARO/")){ + String s = Arrays.asList(topic.split("/")).get(2); + topic = "FIGARO/post/*/"+s; + // log.info("Client received message on [{}] payload [{}] with QoS [{}]", topic, msg.payload().toJsonObject(), msg.qosLevel()); } - JsonObject payload = msg.payload().toJsonObject(); - + // JsonObject payload =msg.payload().toJsonObject(); + // JsonObject payload = parseFrame(msg.payload().getBytes()); + JsonObject payload = parseFrame(processToBytes(msg.payload().getBytes())); + System.out.println("payload原始数据++++++++++++++++++++" + payload); + if(ObjectUtil.isEmpty(payload)){ + return; + } try { //客户端连接断开 if (topic.equals("/sys/client/disconnected")) { @@ -204,27 +211,37 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { IDeviceAction action = null; String method = payload.getString("method", ""); if(StringUtils.isBlank(method)){ - method = "thing.event.property.post_reply"; + method = "thing.event.property.post"; } if (StringUtils.isBlank(method)) { return; } + boolean flag = true; JsonObject params = null; - + String replyHex = null; if(!StringUtils.isBlank(payload.getString("method", ""))){ params = payload.getJsonObject("params", defParams); }else{ String cmd =payload.getString("cmd"); - String data =payload.getString("data"); + String data =payload.getString("data").replaceAll("\\s+", ""); ; + String rawHex =payload.getString("rawHex").replaceAll("\\s+", ""); ; + replyHex = replyToInstructions(payload);//回复的数据 + Map mapRaw = new HashMap<>(); + mapRaw.put("rawHex", rawHex); + mapRaw.put("replyHex", replyHex); + Map result = new HashMap<>(mapRaw); if(ObjectUtils.isNotEmpty(cmd)&& ObjectUtils.isNotEmpty(data)){ - if(cmd.equals("11")){ + if(cmd.equals("0x11")){ + //4.1 状态信息上报(命令码0x11) - Map map = DeviceStatusParser.parseStatusReportToMap(data); + DeviceStatusParser.parseStatusReportToMap(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2) // 冲突时保留后者 + ); // params= payload.getJsonObject(DeviceStatusParser.parseStatusReportToMap(data),defParams); - payload.put("params",map); - device.setDeviceName(map.get("imei").toString()); - switch(map.get("deviceType").toString()) { + payload.put("params",result); + device.setDeviceName(result.get("imei").toString()); + switch(result.get("deviceType").toString()) { case "0": device.setProductKey("CEMpmANABN7Tt6Jh"); @@ -242,26 +259,73 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { break; } - - - }else if(cmd.equals("12")){ + //注册 + IDeviceAction action1 = DeviceRegister.builder() + .model("FIGARO") + .id(UUID.randomUUID().toString()) + .deviceName(device.getDeviceName()) + .productKey(device.getProductKey()) + .params(result) + .time(System.currentTimeMillis()) + .build(); + thingService.post(pluginInfo.getPluginId(), action1); + + + }else if(cmd.equals("0x12")){ //4.2 燃气报警器物联网模块数据上报(0x12) - payload.put("params",GasAlarmDataParser.parseHexDataToList(data)); + GasAlarmDataParser.parseHexDataToList(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2)); + payload.put("params",result); // GasAlarmDataParser.parseHexDataToList(data); - }else if(cmd.equals("14")){ + }else if(cmd.equals("0x14")){ + //动火离人数据上报与事件上报 + FireSafetyDataReceiver.handleHexData(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2)); + payload.put("params",result); } + else if(cmd.equals("0x23")){ + flag=false; + IoTConfigProtocol.parseReadResponse(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2)); + //读取配置数据 + payload.put("params",result); + + //读取配置 + DeviceConfig config = DeviceConfig.builder() + .id(UUID.randomUUID().toString()) + .deviceName(device.getDeviceName()) + .productKey(device.getProductKey()) + .time(System.currentTimeMillis()) + .config(payload.getJsonObject("params", defParams).getMap()) + .build(); + thingService.post(pluginInfo.getPluginId(), config); + + + } + else if(cmd.equals("0x24")){ + flag=false; + //下发配置数据的回复 + // payload.put("params", FireSafetyDataReceiver.handleHexData(data)); + } + } // params = //解析设备数据 } + params = payload.getJsonObject("params", defParams); + System.out.println("params+++++++++-----------++++++++++++++++++++++++++" + params); + if (ObjectUtil.isNull(params) || params.isEmpty()){ + return; + } // 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( @@ -308,7 +372,9 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { } try { - reply(topic, payload, 0); + if(flag) { + reply1(replyHex,device.getDeviceName(), payload); + } }catch (Exception e){ log.error("reply error", e); } @@ -316,7 +382,7 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { if (action == null) { return; } - action.setId(payload.getString("id")); + action.setId(UUID.randomUUID().toString()); action.setProductKey(device.getProductKey()); action.setDeviceName(device.getDeviceName()); action.setTime(System.currentTimeMillis()); @@ -334,11 +400,15 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { } } + + + public ThingDevice getDevice(String topic) { String[] topicParts = topic.split("/"); - if (topicParts.length < 5) { + if (topicParts.length < 4) { return null; } + System.out.println(topicParts[2]); return ThingDevice.builder() .productKey(topicParts[2]) .deviceName(topicParts[3]) @@ -346,9 +416,9 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { } public void online(String pk, String dn) { - if (Boolean.TRUE.equals(DEVICE_ONLINE.get(dn))) { + /* if (Boolean.TRUE.equals(DEVICE_ONLINE.get(dn))) { return; - } + }*/ //上线 thingService.post( @@ -387,7 +457,79 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { action.setTime(System.currentTimeMillis()); return action; } - + private String replyToInstructions(JsonObject payload) { + String mid = payload.getString("messageId"); + String cmd = payload.getString("cmd"); + payload.getString("cmd"); + System.out.println("mid1" + mid); + System.out.println("cmd1" + cmd); + if (mid.toLowerCase().startsWith("0x")) { + mid = mid.substring(2); + } + if (cmd.toLowerCase().startsWith("0x")) { + cmd = cmd.substring(2); + } + cmd = cmd.replaceAll("\\D", ""); + mid = mid.replaceAll("\\D", ""); + if (mid.length() % 2 != 0) { + mid = "0" + mid; // 左侧补零 + } + if (cmd.length() % 2 != 0) { + cmd = "0" + cmd; // 左侧补零 + } + System.out.println("mid" + mid); + System.out.println("cmd" + cmd); + // System.out.println(mid + "re++++++++++++++++++++++++++++++++++++++++++++++++++" + cmd); + // System.out.println("BCD Time: " + formatBCDToHex(generateBCDTime())); + String replyMessage = buildFrame("01", "01", + mid, cmd, "00" + formatBCDToHex(generateBCDTime())); + return replyMessage; + } + /** + * 回复设备 + */ + private void reply1(String replyMessage,String deviceName, JsonObject payload) { + Map payloadReply = new HashMap<>(); + if(ObjectUtil.isNull(replyMessage)) { + String mid = payload.getString("messageId"); + String cmd = payload.getString("cmd"); + payload.getString("cmd"); + System.out.println("mid1" + mid); + System.out.println("cmd1" + cmd); + if (mid.toLowerCase().startsWith("0x")) { + mid = mid.substring(2); + } + if (cmd.toLowerCase().startsWith("0x")) { + cmd = cmd.substring(2); + } + cmd = cmd.replaceAll("\\D", ""); + mid = mid.replaceAll("\\D", ""); + if (mid.length() % 2 != 0) { + mid = "0" + mid; // 左侧补零 + } + if (cmd.length() % 2 != 0) { + cmd = "0" + cmd; // 左侧补零 + } + System.out.println("mid" + mid); + System.out.println("cmd" + cmd); + // System.out.println(mid + "re++++++++++++++++++++++++++++++++++++++++++++++++++" + cmd); + // System.out.println("BCD Time: " + formatBCDToHex(generateBCDTime())); + replyMessage = buildFrame("01", "01", + mid, cmd, "00" + formatBCDToHex(generateBCDTime())); + } + payloadReply.put("data", replyMessage); + String topic="FIGARO/request/" + deviceName; + /* byte[] bytes = replyMessage.getBytes(StandardCharsets.UTF_8); + Buffer buffer = Buffer.buffer(bytes);*/ + client.publish(topic, Buffer.buffer(replyMessage), MqttQoS.AT_LEAST_ONCE, false, false) + .onSuccess(h -> { + log.info("publish {} success", topic); + }); + /* client.publish(topic, JsonObject.mapFrom(payloadReply).toBuffer(), MqttQoS.AT_LEAST_ONCE, false, false) + .onSuccess(h -> { + log.info("publish {} success", finalTopic); + });*/ + } /** * 回复设备 */ @@ -396,9 +538,9 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { // System.out.println("BCD Time: " + formatBCDToHex(generateBCDTime())); String replyMessage = buildFrame("01","01", - "01","01","01"+formatBCDToHex(generateBCDTime())); + "00","01","01"+formatBCDToHex(generateBCDTime())); - if( StringUtils.isBlank(payload.getString("method"))){ + if( StringUtils.isBlank(payload.getString("method"))){ payloadReply.put("id", payload.getString("messageID")); payloadReply.put("method", "thing.event.property.post_reply"); payloadReply.put("code", code); @@ -436,15 +578,17 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { } payloadReply.put("data", replyMessage); String finalTopic = topic; - - client.publish(topic, JsonObject.mapFrom(payloadReply).toBuffer(), MqttQoS.AT_LEAST_ONCE, false, false) + /* String dadad="FIGARO/request/862571079391279"; + byte[] bytes = replyMessage.getBytes(StandardCharsets.UTF_8); + Buffer buffer = Buffer.buffer(bytes);*/ + /* client.publish(topic, payloadReply.get(""), 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) + });*/ + 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) { 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 index f03a3a3..76fca71 100644 --- 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 @@ -12,12 +12,14 @@ 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.buffer.Buffer; 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.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -40,6 +42,7 @@ 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"); @@ -49,47 +52,48 @@ public class MqttDevice implements IDevice { Map map = new HashMap(); String key = entry.getKey(); String value = entry.getValue().toString(); - Integer configId = null; + 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": + switch (key) { + case "ip": configId = IP_REALM; - tlvData = buildTLVData(value.getBytes()); + tlvData = convertToFixedAscii(value,50); break; case "port": - case "keepalive": - case "reportrateupper": + 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)); + configId = (key.equals("port") ? PORT : key.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); + case "reportAwaitTime": + case "reportRetryTimes": + configId = (key.equals("reportAwaitTime") ? REPORT_AWAIT_TIME : REPORT_RETRY_TIMES); tlvData = buildByteData(Integer.valueOf(value)); break; - // case "username": - // case "password": + // 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); + // break; + case "publishTopicPrefix": + case "subscribeTopicPrefix": + configId = (key.equals("subscribeTopicPrefix") ? SUBSCRIBE_TOPIC : PUBLISH_TOPIC); tlvData = buildStringData(value.toString(), 30); break; - case "timesyncinternal": + case "timeSyncInternal": configId = TIME_SYNC; tlvData = buildBcdTimeData(value); break; + case "reportConcentrationRange": + configId = REPORT_CONCENTRATION_RANGE; + tlvData = ByteBuffer.allocate(2).putShort(Short.parseShort(value)).array(); + break; + case "reportRateLower": + configId = REPORT_RATE_LOWER; + tlvData = ByteBuffer.allocate(2).putShort(Short.parseShort(value)).array(); + break; case "card": case "signal": case "version": @@ -99,39 +103,165 @@ public class MqttDevice implements IDevice { 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); - } + if (ObjectUtil.isNotNull(configId)) { + String command = IoTConfigProtocol.generateWriteCommand( + configId, tlvData); + map.put("data", command); + list.add(map); + 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; + } + + public List processGetConfig(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(); + Integer configId = null; + try { + switch (key) { + case "ip": + configId = IP_REALM; + break; + case "port": + configId = PORT; + break; + + case "reportAwaitTime": + configId = REPORT_AWAIT_TIME; + break; + + case "reportRetryTimes": + configId = REPORT_RETRY_TIMES; + break; + case "card": + configId = CARD; + break; + case "username": + configId = USERNAME; + break; + case "password": + configId = PASSWORD; + break; + case "keepAlive": + configId = KEEP_ALIVE; + break; + case "publishTopicPrefix": + configId = PUBLISH_TOPIC; + break; + case "subscribeTopicPrefix": + configId = SUBSCRIBE_TOPIC; + break; + case "reportRateUpper": + configId = REPORT_RATE_UPPER; + break; + case "reportConcentrationRange": + configId = REPORT_CONCENTRATION_RANGE; + break; + case "reportRateLower": + configId = REPORT_RATE_LOWER; + break; + case "signal": + configId = SIGNAL; + break; + case "timeSyncInternal": + configId = TIME_SYNC; + break; + case "version": + configId = VERSION; + break; + default: + System.out.println("Unsupported config: " + key); + continue; + } + if (ObjectUtil.isNotNull(configId)) { + String command = IoTConfigProtocol.generateReadCommand( + configId); + map.put("data", command); + list.add(map); + System.out.println("ddddeeeeeeeeeeeeeeddddd" + map); + /* 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()); } } + System.out.println("ddddddddddddddddddddddddddddddddd" + list); 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) - ); - } + String topic = "FIGARO/request/"+ action.getDeviceName(); + // Map data = JsonUtils.parseObject(action.getConfig(), Map.class); + /* int type = 0x0000; + switch (action.getProductKey()) { + case "CEMpmANABN7Tt6Jh0": + type = 0x0000; + break; + case "XmXYxjzihseT76As": + type = 0x0001; + break; + case "bAASX8tBjYQjBGFP": + type = 0x0002; + break; + case "WfpZZFkMxxbGfRca": + type = 0x0003; + break; + }*/ + System.out.println("action.getProductKey()++++++++++++++++++++" + action.getProductKey()); + if (action.getModule().equals("set")) { + List list = processConfig(action.getConfig()); + System.out.println("listset++++++++++++++++++++" + list); + for (int i = 0; i < list.size(); i++) { + String replyMessage = buildFrame("01","01", + "00", "24", list.get(i).get("data").toString()); + //循环发送配置指令 + /* String replyMessage = buildFrame("01", "01", + "01", "24", "01" + list.get(i).get("data"));*/ + send( + topic,replyMessage + ); + try { + Thread.sleep(500); + }catch (Exception e){ + } + + } + } else if(action.getModule().equals("get")){ + System.out.println("action.getConfig()++++++++++++++++++++" + action.getConfig()); + //配置获取指令下发 + List list = processGetConfig(action.getConfig()); + System.out.println("list++++++++++++++++++++" + list); + for (int i = 0; i < list.size(); i++) { + //循环发送配置指令 + /* String replyMessage = buildFrame("01", "01", + "01", "23", "01" + list.get(i).get("data"));*/ + String replyMessage = buildFrame("01","01", + "00", "23",list.get(i).get("data").toString()); + + send( + topic,replyMessage + ); + try { + Thread.sleep(500); + }catch (Exception e){ + } + } + } return ActionResult.builder().code(0).reason("").build(); } @@ -182,5 +312,17 @@ public class MqttDevice implements IDevice { return ActionResult.builder().code(ErrCode.UNKNOWN_EXCEPTION.getKey()).reason(e.getMessage()).build(); } } + private ActionResult send(String topic, String payload) { + try { + /* byte[] bytes = payload.getBytes(StandardCharsets.UTF_8); + Buffer buffer = Buffer.buffer(bytes);*/ + client.publish(topic, Buffer.buffer(payload), 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/pom.xml b/pom.xml index e16fda1..b486840 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 2.7.11 - 2.10.19 + 2.10.18 cc.iotkit.plugins iot-iita-plugins From febc6291b89cfb707f94fccd7114b5e6fdf4fd59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=B1=BC=E5=B9=B2?= <1810377322@163.com> Date: Tue, 19 Aug 2025 18:11:04 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E8=81=94=E5=8A=A8=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E7=AE=B1=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/emqx/conf/EventTypeMapper.java | 11 +- .../plugins/emqx/conf/GasAlarmDataParser.java | 46 +++- .../plugins/emqx/conf/IoTConfigProtocol.java | 3 + .../emqx/conf/LinkageControlDataParser.java | 248 ++++++++++++++++++ .../plugins/emqx/conf/ProtocolTest.java | 75 +++++- .../plugins/emqx/service/EmqxPlugin.java | 49 +++- .../plugins/emqx/service/MqttDevice.java | 14 +- .../src/main/resources/application.yml | 6 +- 8 files changed, 420 insertions(+), 32 deletions(-) create mode 100644 emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/LinkageControlDataParser.java 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 index 397c573..113dd07 100644 --- 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 @@ -2,9 +2,10 @@ package cc.iotkit.plugins.emqx.conf; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class EventTypeMapper { - private static final Map EVENT_TYPE_MAP = new HashMap<>(); + private static final Map EVENT_TYPE_MAP = new ConcurrentHashMap<>(); static { // 节点相关事件 @@ -23,7 +24,11 @@ public class EventTypeMapper { 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(30, "自检"); EVENT_TYPE_MAP.put(31, "备电故障"); @@ -47,6 +52,8 @@ public class EventTypeMapper { // 正常上报事件 EVENT_TYPE_MAP.put(128, "燃气报警器正常数据上报"); EVENT_TYPE_MAP.put(129, "动火离人正常数据上报"); + EVENT_TYPE_MAP.put(130, "联动控制箱正常数据上报"); + EVENT_TYPE_MAP.put(151, "上报输出反馈状态变化"); } public static String getEventTypeName(int 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 index d0b9210..2fe4d8c 100644 --- 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 @@ -1,5 +1,7 @@ package cc.iotkit.plugins.emqx.conf; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; import java.time.LocalDateTime; @@ -121,8 +123,9 @@ public class GasAlarmDataParser { 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()))); + int eventType = buffer.getShort() & 0xFFFF; + eventMap.put("eventType", eventType); + eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(eventType)); eventMap.put("timestamp", parseBcdTime(buffer)); // eventList.add(eventMap); @@ -135,19 +138,30 @@ public class GasAlarmDataParser { Map eventMap = new LinkedHashMap<>(); eventMap.put("dataType", "NODE_EVENT"); - eventMap.put("eventType", buffer.getShort() & 0xFFFF); + int eventType = buffer.getShort() & 0xFFFF; + eventMap.put("eventType", eventType); if (!Objects.equals(eventMap.get("eventType"), 15)) { - eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(Integer.parseInt(eventMap.get("eventType").toString()))); + eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(eventType)); eventMap.put("timestamp", parseBcdTime(buffer)); eventMap.put("nodeId", buffer.getShort() & 0xFFFF); - eventMap.put("dataValue", buffer.getShort() & 0xFFFF); + // BigDecimal decimalValue = new BigDecimal(buffer.getShort() & 0xFFFF) + // .divide(new BigDecimal(1), 4, RoundingMode.HALF_UP).stripTrailingZeros(); + eventMap.put("dataValues", buffer.getShort() & 0xFFFF); + // eventMap.put("dataDetectorValue" , eventMap.get("dataValues")); } else { - eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(Integer.parseInt(eventMap.get("eventType").toString()))); + eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(eventType)); eventMap.put("timestamp", parseBcdTime(buffer)); - eventMap.put("totalNodes", buffer.getShort() & 0xFF); + eventMap.put("totalNodes", buffer.get() & 0xFF); eventMap.put("nodeId", buffer.getShort() & 0xFFFF); + System.out.println("浓度变化的剩余长度"+buffer.remaining()); + // int nodeCounter = 0; while (buffer.remaining() >= 2) { - eventMap.put("dataValue", buffer.getShort() & 0xFFFF); + // String suffix = String.valueOf(nodeCounter++); + // BigDecimal decimalValue = new BigDecimal(buffer.getShort() & 0xFFFF) + // .divide(new BigDecimal(1), 4, RoundingMode.HALF_UP).stripTrailingZeros(); + eventMap.put("dataValues", buffer.getShort() & 0xFFFF); + // 0:节点为探测器 + eventMap.put("dataDetectorValue" , eventMap.get("dataValues")); } } @@ -164,8 +178,9 @@ public class GasAlarmDataParser { // 基础信息 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()))); + int eventType = buffer.getShort() & 0xFFFF; + baseInfo.put("eventType", eventType); + baseInfo.put("eventTypeValue", EventTypeMapper.getEventTypeName(eventType)); baseInfo.put("timestamp", parseBcdTime(buffer)); baseInfo.put("totalNodes", buffer.getShort() & 0xFFFF); baseInfo.put("reportedNodes", buffer.get() & 0xFF); @@ -182,13 +197,18 @@ public class GasAlarmDataParser { // nodeMap.put("unit", buffer.get() & 0xFF); // 节点节点参数 // nodeMap.put("precision", buffer.get() & 0xFF); baseInfo.put("gasType" + suffix, GasDetectorParser.parse(buffer.getShort() & 0xFFFF)); - baseInfo.put("dataValue" + suffix, buffer.getShort() & 0xFFFF); + Integer dataValue = buffer.getShort() & 0xFFFF; if (Objects.equals(baseInfo.get("nodeType" + suffix), 0)) { + // BigDecimal decimalValue = new BigDecimal(dataValue) + // .divide(new BigDecimal(1), 4, RoundingMode.HALF_UP).stripTrailingZeros(); + baseInfo.put("dataValues" + suffix, dataValue); // 0:节点为探测器 - baseInfo.put("dataTypeValue" + suffix, baseInfo.get("dataValue" + suffix)); + baseInfo.put("dataDetectorValue" + suffix, baseInfo.get("dataValues" + suffix)); } else if (Objects.equals(baseInfo.get("nodeType" + suffix), 1)) { + + baseInfo.put("dataValues" + suffix, dataValue); //1:节点为输出模块 - baseInfo.put("dataTypeValue" + suffix, baseInfo.get("dataValue" + suffix).equals("0") ? "未动作" : "已动作"); + baseInfo.put("dataSinksValue" + suffix, baseInfo.get("dataValues" + suffix).equals("0") ? "未动作" : "已动作"); } // dataList.add(nodeMap); } 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 index 507d86b..fefc393 100644 --- 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 @@ -25,6 +25,7 @@ public class IoTConfigProtocol { public static final int SIGNAL = 0x0024; public static final int TIME_SYNC = 0x0025; public static final int VERSION = 0x0026; + public static final int REPORT_RATE_LinkConstraint = 0x0100; // 生成读取指令 // 反向映射Map private static final Map CONFIG_ID_TO_KEY = new HashMap<>(); @@ -47,6 +48,8 @@ public class IoTConfigProtocol { CONFIG_ID_TO_KEY.put(SIGNAL, "signal"); CONFIG_ID_TO_KEY.put(TIME_SYNC, "timeSyncInternal"); CONFIG_ID_TO_KEY.put(VERSION, "version"); + CONFIG_ID_TO_KEY.put(REPORT_RATE_LinkConstraint, "controlOutput"); + } public static String getKeyByConfigId(int configId) { diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/LinkageControlDataParser.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/LinkageControlDataParser.java new file mode 100644 index 0000000..51146a6 --- /dev/null +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/LinkageControlDataParser.java @@ -0,0 +1,248 @@ +package cc.iotkit.plugins.emqx.conf; + +import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; +import java.util.*; + +public class LinkageControlDataParser { + + private static void testEventGroup(String groupName, String[] testCases) { + System.out.println("\n=== " + groupName + "测试 ==="); + for (String hexData : testCases) { + try { + Map result = LinkageControlDataParser.parseHexDataToList(hexData); + System.out.println("原始数据: " + hexData); + System.out.println("解析结果: " + result.get("eventType") + + " - " + result.get("dataType")); + System.out.println("rrr解析结果: " + result.get(0)); + + + } catch (Exception e) { + System.out.println("解析异常: " + e.getMessage()); + } + } + } + + public static Map parseHexDataToList(String hexData) { + System.out.println("parseHexDataToList: " + hexData); + byte[] data = hexStringToByteArray(hexData); + return parseDataToList(data); + } + + public static Map parseDataToList(byte[] data) { + Map resultList = new HashMap<>(); + if (data == null || data.length < 1) { + return resultList; + } + + int eventType = Byte.toUnsignedInt(data[0]) + Byte.toUnsignedInt(data[1]); + System.out.println("eventType: " + eventType); + if (eventType == 130) { + // // 正常数据上报 (NORMAL_DATA) + return parseNormalDataToList(data); + } /*else if (isControllerEvent(eventType)) { + + // 控制器事件 (CONTROLLER_EVENT) + return parseControllerEventToList(data); + }*/ + + // 节点事件 (NODE_EVENT) + return LinkageControlEventToList(data); + } + + private static Map LinkageControlEventToList(byte[] data) { + // List> eventList = new ArrayList<>(); + ByteBuffer buffer = ByteBuffer.wrap(data); + Map eventMap = new LinkedHashMap<>(); + + eventMap.put("dataType", "LINKAGE_CONTROL_EVENT"); + int eventType = buffer.getShort() & 0xFFFF; + eventMap.put("eventType", eventType); + eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(eventType)); + eventMap.put("timestamp", parseBcdTime(buffer)); + eventMap.put("nodeId", buffer.getShort() & 0xFFFF); + Integer dataValue = buffer.getShort() & 0xFFFF; + eventMap.put("dataValues", dataValue); + String status = (dataValue == 0) ? "不支持反馈" : + (dataValue == 1) ? "反馈未动作" : + (dataValue == 2) ? "反馈动作" : "未知状态"; + eventMap.put("dataDetectorValue" , status); + return eventMap; + } + + private static Map parseNodeEventToList(byte[] data) { + // Map eventList = new HashMap<>(); + ByteBuffer buffer = ByteBuffer.wrap(data); + Map eventMap = new LinkedHashMap<>(); + + eventMap.put("dataType", "NODE_EVENT"); + int eventType = buffer.getShort() & 0xFFFF; + eventMap.put("eventType", eventType); + if (!Objects.equals(eventMap.get("eventType"), 15)) { + eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(eventType)); + eventMap.put("timestamp", parseBcdTime(buffer)); + eventMap.put("nodeId", buffer.getShort() & 0xFFFF); + // BigDecimal decimalValue = new BigDecimal(buffer.getShort() & 0xFFFF) + // .divide(new BigDecimal(1), 4, RoundingMode.HALF_UP).stripTrailingZeros(); + eventMap.put("dataValues", buffer.getShort() & 0xFFFF); + // eventMap.put("dataDetectorValue" , eventMap.get("dataValues")); + } else { + eventMap.put("eventTypeValue", EventTypeMapper.getEventTypeName(eventType)); + eventMap.put("timestamp", parseBcdTime(buffer)); + eventMap.put("totalNodes", buffer.get() & 0xFF); + eventMap.put("nodeId", buffer.getShort() & 0xFFFF); + System.out.println("浓度变化的剩余长度"+buffer.remaining()); + // int nodeCounter = 0; + while (buffer.remaining() >= 2) { + // String suffix = String.valueOf(nodeCounter++); + // BigDecimal decimalValue = new BigDecimal(buffer.getShort() & 0xFFFF) + // .divide(new BigDecimal(1), 4, RoundingMode.HALF_UP).stripTrailingZeros(); + eventMap.put("dataValues", buffer.getShort() & 0xFFFF); + // 0:节点为探测器 + eventMap.put("dataDetectorValue" , eventMap.get("dataValues")); + } + + } + System.out.println("eventMap+++++++++++: " + eventMap); + //eventList.add(eventMap); + + return eventMap; + } + + private static Map parseNormalDataToList(byte[] data) { + // List> dataList = new ArrayList<>(); + ByteBuffer buffer = ByteBuffer.wrap(data); + + // 基础信息 + Map baseInfo = new LinkedHashMap<>(); + baseInfo.put("dataType", "Linkage_DATA"); + int eventType = buffer.getShort() & 0xFFFF; + baseInfo.put("eventType", eventType); + baseInfo.put("eventTypeValue", EventTypeMapper.getEventTypeName(eventType)); + baseInfo.put("timestamp", parseBcdTime(buffer)); + // baseInfo.put("totalNodes", buffer.getShort() & 0xFFFF); + baseInfo.put("reportedNodes", buffer.get() & 0xFF); + // dataList.add(baseInfo); + System.out.println(buffer.remaining()); + // 节点数据 + // 节点数据(动态添加数字后缀) + int nodeCounter = 0; + while (buffer.remaining() >= 7) { + String suffix = String.valueOf(nodeCounter++); + // Map nodeMap = new LinkedHashMap<>(); + baseInfo.put("nodeId" + suffix, buffer.getShort() & 0xFFFF); + // baseInfo.put("nodeType" + suffix, buffer.get() & 0xFF); + // nodeMap.put("unit", buffer.get() & 0xFF); // 节点节点参数 + // nodeMap.put("precision", buffer.get() & 0xFF); + // baseInfo.put("gasType" + suffix, GasDetectorParser.parse(buffer.getShort() & 0xFFFF)); + Integer dataValue = buffer.getShort() & 0xFFFF; + // if (Objects.equals(baseInfo.get("nodeType" + suffix), 0)) { + // BigDecimal decimalValue = new BigDecimal(dataValue) + // .divide(new BigDecimal(1), 4, RoundingMode.HALF_UP).stripTrailingZeros(); + baseInfo.put("dataValues" + suffix, dataValue); + // 0:节点为探测器 + // if (Objects.equals(baseInfo.get("nodeType" + suffix), 0)) { + String status = (dataValue == 0) ? "不支持反馈" : + (dataValue == 1) ? "反馈未动作" : + (dataValue == 2) ? "反馈动作" : "未知状态"; + baseInfo.put("dataDetectorValue" + suffix, status); + // } else if (Objects.equals(baseInfo.get("nodeType" + suffix), 1)) { + + // baseInfo.put("dataValues" + suffix, dataValue); + //1:节点为输出模块 + // baseInfo.put("dataSinksValue" + suffix, baseInfo.get("dataValues" + suffix).equals("0") ? "未动作" : "已动作"); + // } + // dataList.add(nodeMap); + } + + return baseInfo; + } + // 辅助方法:字节数组转十六进制字符串 + + private static String bytesToHex(byte[] bytes) { + + StringBuilder hexSb = new StringBuilder(); + for (byte b : bytes) { + hexSb.append(String.format("%02X", b)); + } + return hexSb.toString(); + + } + // 保留原有辅助方法 + /*public static String parseBcdTime(ByteBuffer buffer) { + // 打印ByteBuffer的十六进制表示 + System.out.println("ByteBuffer内容(十六进制): " + bytesToHex(buffer.array())); + + byte[] bcdTime = new byte[6]; + + buffer.get(bcdTime); + + StringBuilder sb = new StringBuilder(); + + for (byte b : bcdTime) { + + // 提取高四位和低四位,并转换为十进制数字 + + int high = (b >> 4) & 0x0F; + + int low = b & 0x0F; + + // 组合成两位十进制字符串 + + sb.append(String.format("%1d%1d", high, low)); + + } + + String bcdStr = sb.toString(); + + try { + + SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmss"); + + sdf.set2DigitYearStart(new Date(946684800000L)); // 2000-01-01 + + Date date = sdf.parse(bcdStr); + + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); + + } catch (Exception e) { + + return bcdStr; // 返回原始BCD字符串(可能包含越界值) + + } + }*/ + + public static String parseBcdTime(ByteBuffer buffer) { + // 打印ByteBuffer的十六进制表示 + System.out.println("ByteBuffer内容(十六进制): " + bytesToHex(buffer.array())); + + 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"); + sdf.set2DigitYearStart(new SimpleDateFormat("yyyy").parse("2000")); // 设置世纪转折点 + 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 >= 30 && eventType <= 38; + } +} diff --git a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ProtocolTest.java b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ProtocolTest.java index 7340762..ab8a212 100644 --- a/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ProtocolTest.java +++ b/emqx-plugin/src/main/java/cc/iotkit/plugins/emqx/conf/ProtocolTest.java @@ -3,13 +3,16 @@ package cc.iotkit.plugins.emqx.conf; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static cc.iotkit.plugins.emqx.conf.CRC16ModbusUtil.buildFrame; import static cc.iotkit.plugins.emqx.conf.IoTConfigProtocol.*; @@ -70,9 +73,75 @@ public class ProtocolTest { return ByteBuffer.wrap(bytes); } + public static String formatHexByte(String input) { + // 移除0x前缀并保留纯十六进制字符 + String hex = input.replaceAll("(?i)^0x", "").replaceAll("[^0-9a-fA-F]", ""); + // 标准化为2位十六进制(不足补零,超长截断) + return hex.isEmpty() ? "00" + : hex.length() == 1 ? "0" + hex + : hex.substring(hex.length() - 2); + } + public static void extractDetectorData(Map baseInfo) { + for (Map.Entry entry : baseInfo.entrySet()) { + if (entry.getKey().startsWith("dataDetectorValue")) { + String suffix = entry.getKey().substring(17); // 去掉前缀"dataDetectorValue" + Integer value = (Integer) entry.getValue(); + + // 获取对应节点信息 + String nodeIdKey = "nodeId" + suffix; + String gasTypeKey = "gasType" + suffix; + + System.out.println("NodeID: " + baseInfo.get(nodeIdKey) + + ", GasType: " + baseInfo.get(gasTypeKey) + + ", DetectorValue: " + value); + } + } + } + public static String generateWriteCommand(int configId, byte[] data) { + + ByteBuffer buf = ByteBuffer.allocate(3 + 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()); + } + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + } + return sb.toString(); + } public static void main(String[] args) { + String dsada=null; + System.out.println(dsada.startsWith("https")); + System.out.println(generateWriteCommand(0x0100,ByteBuffer.allocate(4) + .order(ByteOrder.BIG_ENDIAN) + .putInt(Integer.parseUnsignedInt("00010001", 16)).array())); + System.out.println(ByteBuffer.allocate(2).putShort(Short.parseShort("1")).array()); + System.out.println(EventTypeMapper.getEventTypeName(Integer.parseInt("20"))); + BigDecimal decimalValue = new BigDecimal("1123.391111111111111") + .divide(new BigDecimal(1), 4, RoundingMode.HALF_UP).stripTrailingZeros(); + + System.out.println(decimalValue); + Map baseInfo = new HashMap<>(); + baseInfo.put("nodeId", 12345); + baseInfo.put("nodeType", 0); // 探测器 + baseInfo.put("dataDetectorValue", 0); + baseInfo.put("gasType", "ddddddddddddddddd"); + baseInfo.put("dataValue", "ddddddddddaaaa"); // 探测器值 + baseInfo.put("nodeId0111", 67890); + baseInfo.put("nodeType0111", 1); // 输出模块 + baseInfo.put("dataDetectorValue0111", 0); + baseInfo.put("gasType0111", "dasa12321"); + baseInfo.put("dataValue0111", "Ddsa"); + extractDetectorData(baseInfo); + System.out.println(formatHexByte("0x014")); // GasAlarmDataParser.parseBcdTime(createBcdTimeBuffer("250707131758")); System.out.println(GasAlarmDataParser.parseBcdTime(createBcdTimeBuffer("250703103146"))); //String url = "https://minio.dddd.com/feijialuo/20250702/lQLPKHYULtpjo8HM_Mz8sCPMoZZYPmEbCD1keBQpVgA_252_2521751416904105.png"; 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 index e4d0e28..f71bdd2 100644 --- 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 @@ -99,8 +99,8 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { String serverPassword = IdUtil.fastSimpleUUID(); MqttClientOptions options = new MqttClientOptions() - .setClientId("server11223") - .setUsername("server11223") + .setClientId("server112233") + .setUsername("server112233") .setPassword(serverPassword) .setCleanSession(true) .setMaxInflightQueue(100) @@ -244,19 +244,23 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { switch(result.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"); - + case "4": + //联动控制箱设备 + System.out.println("联动控制箱设备注册++++++++++++++++++++++++++++++++++++++++++"); + device.setProductKey("MQFejp7cyDMH3enG"); break; } //注册 @@ -282,6 +286,11 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { FireSafetyDataReceiver.handleHexData(data).forEach( (key, value) -> result.merge(key, value, (v1, v2) -> v2)); payload.put("params",result); + }else if(cmd.equals("0x15")){ + //联动控制箱设备状态及参数数据上报 + LinkageControlDataParser.parseHexDataToList(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2)); + payload.put("params",result); } else if(cmd.equals("0x23")){ flag=false; @@ -460,8 +469,8 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { private String replyToInstructions(JsonObject payload) { String mid = payload.getString("messageId"); String cmd = payload.getString("cmd"); - payload.getString("cmd"); - System.out.println("mid1" + mid); + //payload.getString("cmd"); + /* System.out.println("mid1" + mid); System.out.println("cmd1" + cmd); if (mid.toLowerCase().startsWith("0x")) { mid = mid.substring(2); @@ -471,12 +480,22 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { } cmd = cmd.replaceAll("\\D", ""); mid = mid.replaceAll("\\D", ""); + if (cmd.length() == 0) { + cmd = "00"; // 空值默认补零 + } else if (cmd.length() == 1) { + cmd = "0" + cmd; // 单字符左侧补零 + } else if (cmd.length() > 2) { + cmd = cmd.slice(-2); // 超长取最后两位 + } + if (mid.length() % 2 != 0) { mid = "0" + mid; // 左侧补零 } if (cmd.length() % 2 != 0) { cmd = "0" + cmd; // 左侧补零 - } + }*/ + cmd = formatHexByte(cmd); + mid = formatHexByte(mid); System.out.println("mid" + mid); System.out.println("cmd" + cmd); // System.out.println(mid + "re++++++++++++++++++++++++++++++++++++++++++++++++++" + cmd); @@ -485,7 +504,17 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { mid, cmd, "00" + formatBCDToHex(generateBCDTime())); return replyMessage; } - /** + public static String formatHexByte(String input) { + // 移除0x前缀并保留纯十六进制字符 + String hex = input.replaceAll("(?i)^0x", "").replaceAll("[^0-9a-fA-F]", ""); + + // 标准化为2位十六进制(不足补零,超长截断) + return hex.isEmpty() ? "00" + : hex.length() == 1 ? "0" + hex + : hex.substring(hex.length() - 2); + } + + /** * 回复设备 */ private void reply1(String replyMessage,String deviceName, JsonObject payload) { 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 index 76fca71..9d6bf32 100644 --- 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 @@ -19,6 +19,7 @@ import lombok.Setter; import org.springframework.stereotype.Service; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; @@ -94,6 +95,14 @@ public class MqttDevice implements IDevice { configId = REPORT_RATE_LOWER; tlvData = ByteBuffer.allocate(2).putShort(Short.parseShort(value)).array(); break; + + case "controlOutput": + configId = REPORT_RATE_LinkConstraint; + tlvData = ByteBuffer.allocate(4) + .order(ByteOrder.BIG_ENDIAN) + .putInt(Integer.parseUnsignedInt(value, 16)).array(); + break; + case "card": case "signal": case "version": @@ -180,6 +189,9 @@ public class MqttDevice implements IDevice { case "version": configId = VERSION; break; + case "controlOutput": + configId = REPORT_RATE_LinkConstraint; + break; default: System.out.println("Unsupported config: " + key); continue; @@ -228,10 +240,10 @@ public class MqttDevice implements IDevice { for (int i = 0; i < list.size(); i++) { String replyMessage = buildFrame("01","01", "00", "24", list.get(i).get("data").toString()); - //循环发送配置指令 /* String replyMessage = buildFrame("01", "01", "01", "24", "01" + list.get(i).get("data"));*/ + System.out.println("配置指令" + replyMessage); send( topic,replyMessage ); diff --git a/emqx-plugin/src/main/resources/application.yml b/emqx-plugin/src/main/resources/application.yml index f313a37..ca8141e 100644 --- a/emqx-plugin/src/main/resources/application.yml +++ b/emqx-plugin/src/main/resources/application.yml @@ -1,9 +1,9 @@ plugin: - runMode: dev + runMode: prod mainPackage: cc.iotkit.plugin emqx: - host: 127.0.0.1 + host: 123.57.78.108 port: 1883 - topics: /sys/# + topics: FIGARO/# authPort: 8104 From 2402bd81df060bbae214b271e8ccf7632f7aefe3 Mon Sep 17 00:00:00 2001 From: alsszer Date: Mon, 9 Feb 2026 14:13:28 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat(plugin):=20=E4=BC=98=E5=8C=96EMQX?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=8A=9F=E8=83=BD=E5=B9=B6=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在application.yml中将运行模式从prod改为dev - 在application-dev.yml中添加generate_statistics配置项以关闭统计信息 - 在AuthVerticle.java中添加MQTT ACL主题路径转换逻辑 - 在DeviceInfoDataCache.java和DeviceInfoDataImpl.java中增加设备状态查询方法 - 在DeviceInfoExpordVo和DeviceInfoImportVo中新增site字段用于设备位置导出导入 - 在DeviceInstallInfo.java中添加多个图片相关字段如泡沫水测漏图片、设备信息图片、门牌号图片 - 在DeviceInstallInfoDataImpl.java中实现findById方法 - 在DeviceInstallInfoExpordVo和DeviceInstallInfoVo中添加切断阀编号和报警器编号导出功能 - 在DeviceInstallInfoServiceImpl.java中完善安装信息查询逻辑 - 在DeviceManagerServiceImpl.java中支持设备状态查询参数 - 在DeviceQueryBo.java中新增deviceStatus字段用于设备状态筛选 - 在feijialuo-emqx-plugin中优化线程池配置并提升并发处理能力 - 在iot-iita-emqx-plugin中调整设备识别规则适配不同平台需求 --- emqx-plugin/pom.xml | 4 +- .../plugins/emqx/service/EmqxPlugin.java | 333 +++++++++++------- .../src/main/resources/application.yml | 2 +- pom.xml | 4 +- 4 files changed, 202 insertions(+), 141 deletions(-) diff --git a/emqx-plugin/pom.xml b/emqx-plugin/pom.xml index 54ad1bc..4b797e4 100644 --- a/emqx-plugin/pom.xml +++ b/emqx-plugin/pom.xml @@ -5,7 +5,7 @@ iot-iita-plugins cc.iotkit.plugins - 2.10.18 + 2.10.30 4.0.0 @@ -83,4 +83,4 @@ - \ No newline at end of file + 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 index f71bdd2..6da1f37 100644 --- 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 @@ -36,10 +36,7 @@ import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import static cc.iotkit.plugins.emqx.conf.BCDClockGenerator.formatBCDToHex; import static cc.iotkit.plugins.emqx.conf.BCDClockGenerator.generateBCDTime; @@ -89,6 +86,18 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { public static final Map> CLIENT_DEVICE_MAP = new HashMap<>(); + //定义一个线程池,线程池大小时cpu的2倍 + // 创建线程池,大小为CPU核心数的2倍 + private static ExecutorService statusThreadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); + // 创建线程池,大小为CPU核心数的2倍 + private static ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); + + // 创建线程池,大小为CPU核心数的2倍 + private static ExecutorService sxsbThreadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); + + // 创建线程池,大小为CPU核心数的2倍 + private static ExecutorService onlineThreadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); + @PostConstruct public void init() { vertx = Vertx.vertx(); @@ -175,6 +184,12 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { log.error("client connect fail: ", s.cause()); } }).publishHandler(msg -> { + // 获取当前线程对象 + Thread currentThread = Thread.currentThread(); + // 获取当前线程的 ID + long threadId = currentThread.getId(); + long start = System.currentTimeMillis() ; + log.info("threadId=189{}",threadId+":"+start); String topic = msg.topicName(); log.info("topic={}",topic); if (topic.contains("/request/")) { @@ -183,15 +198,15 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { if (topic != null && topic.startsWith("FIGARO/")){ String s = Arrays.asList(topic.split("/")).get(2); topic = "FIGARO/post/*/"+s; - // log.info("Client received message on [{}] payload [{}] with QoS [{}]", topic, msg.payload().toJsonObject(), msg.qosLevel()); + // log.info("Client received message on [{}] payload [{}] with QoS [{}]", topic, msg.payload().toJsonObject(), msg.qosLevel()); } - // JsonObject payload =msg.payload().toJsonObject(); - // JsonObject payload = parseFrame(msg.payload().getBytes()); + // JsonObject payload =msg.payload().toJsonObject(); + // JsonObject payload = parseFrame(msg.payload().getBytes()); JsonObject payload = parseFrame(processToBytes(msg.payload().getBytes())); System.out.println("payload原始数据++++++++++++++++++++" + payload); - if(ObjectUtil.isEmpty(payload)){ - return; - } + if(ObjectUtil.isEmpty(payload)){ + return; + } try { //客户端连接断开 if (topic.equals("/sys/client/disconnected")) { @@ -203,7 +218,8 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { if (device == null) { return; } - + long start0 = System.currentTimeMillis() ; + log.info("threadId=222{}",threadId+":"+start0); //有消息上报-设备上线 online("*", device.getDeviceName()); @@ -213,7 +229,8 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { if(StringUtils.isBlank(method)){ method = "thing.event.property.post"; } - + long start1 = System.currentTimeMillis() ; + log.info("threadId=229{}",threadId+":"+start1); if (StringUtils.isBlank(method)) { return; } @@ -231,94 +248,121 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { mapRaw.put("rawHex", rawHex); mapRaw.put("replyHex", replyHex); Map result = new HashMap<>(mapRaw); + long start2 = System.currentTimeMillis() ; + log.info("threadId=248{}",threadId+":"+start2); if(ObjectUtils.isNotEmpty(cmd)&& ObjectUtils.isNotEmpty(data)){ - if(cmd.equals("0x11")){ - - //4.1 状态信息上报(命令码0x11) - DeviceStatusParser.parseStatusReportToMap(data).forEach( - (key, value) -> result.merge(key, value, (v1, v2) -> v2) // 冲突时保留后者 - ); - // params= payload.getJsonObject(DeviceStatusParser.parseStatusReportToMap(data),defParams); - payload.put("params",result); - device.setDeviceName(result.get("imei").toString()); - switch(result.get("deviceType").toString()) { - case "0": - device.setProductKey("CEMpmANABN7Tt6Jh"); - //家用报警器 - break; - case "1": - device.setProductKey("XmXYxjzihseT76As"); - //独立式报警器 - break; - case "2": - device.setProductKey("bAASX8tBjYQjBGFP"); + if(cmd.equals("0x11")){ + + //4.1 状态信息上报(命令码0x11) + DeviceStatusParser.parseStatusReportToMap(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2) // 冲突时保留后者 + ); + // params= payload.getJsonObject(DeviceStatusParser.parseStatusReportToMap(data),defParams); + payload.put("params",result); + device.setDeviceName(result.get("imei").toString()); + switch(result.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"); - case "4": - //联动控制箱设备 - System.out.println("联动控制箱设备注册++++++++++++++++++++++++++++++++++++++++++"); - device.setProductKey("MQFejp7cyDMH3enG"); - break; - } - //注册 - IDeviceAction action1 = DeviceRegister.builder() - .model("FIGARO") - .id(UUID.randomUUID().toString()) - .deviceName(device.getDeviceName()) - .productKey(device.getProductKey()) - .params(result) - .time(System.currentTimeMillis()) - .build(); - thingService.post(pluginInfo.getPluginId(), action1); - - - }else if(cmd.equals("0x12")){ - //4.2 燃气报警器物联网模块数据上报(0x12) - GasAlarmDataParser.parseHexDataToList(data).forEach( - (key, value) -> result.merge(key, value, (v1, v2) -> v2)); - payload.put("params",result); - // GasAlarmDataParser.parseHexDataToList(data); - }else if(cmd.equals("0x14")){ - //动火离人数据上报与事件上报 - FireSafetyDataReceiver.handleHexData(data).forEach( - (key, value) -> result.merge(key, value, (v1, v2) -> v2)); - payload.put("params",result); - }else if(cmd.equals("0x15")){ - //联动控制箱设备状态及参数数据上报 - LinkageControlDataParser.parseHexDataToList(data).forEach( - (key, value) -> result.merge(key, value, (v1, v2) -> v2)); - payload.put("params",result); - } - else if(cmd.equals("0x23")){ - flag=false; - IoTConfigProtocol.parseReadResponse(data).forEach( - (key, value) -> result.merge(key, value, (v1, v2) -> v2)); - //读取配置数据 - payload.put("params",result); - - //读取配置 - DeviceConfig config = DeviceConfig.builder() - .id(UUID.randomUUID().toString()) - .deviceName(device.getDeviceName()) - .productKey(device.getProductKey()) - .time(System.currentTimeMillis()) - .config(payload.getJsonObject("params", defParams).getMap()) - .build(); - thingService.post(pluginInfo.getPluginId(), config); - - - } - else if(cmd.equals("0x24")){ - flag=false; - //下发配置数据的回复 - // payload.put("params", FireSafetyDataReceiver.handleHexData(data)); - } + break; + case "3": + //动火离人设备 + device.setProductKey("WfpZZFkMxxbGfRca"); + case "4": + //联动控制箱设备 + System.out.println("联动控制箱设备注册++++++++++++++++++++++++++++++++++++++++++"); + device.setProductKey("MQFejp7cyDMH3enG"); + break; + } + //注册 + IDeviceAction action1 = DeviceRegister.builder() + .model("FIGARO") + .id(UUID.randomUUID().toString()) + .deviceName(device.getDeviceName()) + .productKey(device.getProductKey()) + .params(result) + .time(System.currentTimeMillis()) + .build(); + long start3= System.currentTimeMillis() ; + log.info("threadId=291{}",threadId+":"+start3); + statusThreadPool.submit(new Runnable() { + @Override + public void run() { + thingService.post(pluginInfo.getPluginId(), action1); + } + }); + long start4= System.currentTimeMillis() ; + log.info("threadId=299{}",threadId+":"+start4); + }else if(cmd.equals("0x12")){ + //4.2 燃气报警器物联网模块数据上报(0x12) + GasAlarmDataParser.parseHexDataToList(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2)); + payload.put("params",result); + long start5= System.currentTimeMillis() ; + log.info("threadId=306{}",threadId+":"+start5); + // GasAlarmDataParser.parseHexDataToList(data); + }else if(cmd.equals("0x14")){ + //动火离人数据上报与事件上报 + FireSafetyDataReceiver.handleHexData(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2)); + payload.put("params",result); + long start6= System.currentTimeMillis() ; + log.info("threadId=314{}",threadId+":"+start6); + }else if(cmd.equals("0x15")){ + //联动控制箱设备状态及参数数据上报 + LinkageControlDataParser.parseHexDataToList(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2)); + payload.put("params",result); + long start7= System.currentTimeMillis() ; + log.info("threadId=321{}",threadId+":"+start7); + } + else if(cmd.equals("0x23")){ + flag=false; + IoTConfigProtocol.parseReadResponse(data).forEach( + (key, value) -> result.merge(key, value, (v1, v2) -> v2)); + //读取配置数据 + payload.put("params",result); + + //读取配置 + DeviceConfig config = DeviceConfig.builder() + .id(UUID.randomUUID().toString()) + .deviceName(device.getDeviceName()) + .productKey(device.getProductKey()) + .time(System.currentTimeMillis()) + .config(payload.getJsonObject("params", defParams).getMap()) + .build(); + long start8= System.currentTimeMillis() ; + log.info("threadId=339{}",threadId+":"+start8); + threadPool.submit(new Runnable() { + @Override + public void run() { + thingService.post(pluginInfo.getPluginId(), config); + } + }); + long start9= System.currentTimeMillis() ; + log.info("threadId=347{}",threadId+":"+start9); + } + else if(cmd.equals("0x24")){ + flag=false; + //下发配置数据的回复 +// Map mapConfig = FireSafetyDataReceiver.handleHexConfigData(data); +// if(!mapConfig.isEmpty()) { +// payload.put("params", mapConfig); +// } +// long start10= System.currentTimeMillis() ; +// log.info("threadId=357{}",threadId+":"+start10); + } } - // params = + // params = //解析设备数据 } @@ -328,16 +372,15 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { if (ObjectUtil.isNull(params) || params.isEmpty()){ return; } - // System.out.println(payload.getJsonObject("data", 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( + long start11= System.currentTimeMillis() ; + log.info("threadId=378{}",threadId+":"+start11); + ActionResult regResult = thingService.post(pluginInfo.getPluginId(),fillAction( SubDeviceRegister.builder() .productKey(device.getProductKey()) .deviceName(device.getDeviceName()) @@ -352,6 +395,8 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { .build() ) ); + long start12= System.currentTimeMillis() ; + log.info("threadId=395{}",threadId+":"+start12); if (regResult.getCode() == 0) { //注册成功 reply(topic, payload, 0); @@ -379,7 +424,8 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { .params(params.getMap()) .build(); } - + long start13= System.currentTimeMillis() ; + log.info("threadId=424{}",threadId+":"+start13); try { if(flag) { reply1(replyHex,device.getDeviceName(), payload); @@ -395,8 +441,17 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { action.setProductKey(device.getProductKey()); action.setDeviceName(device.getDeviceName()); action.setTime(System.currentTimeMillis()); - thingService.post(pluginInfo.getPluginId(), action); - + long start14= System.currentTimeMillis() ; + log.info("threadId=441{}",threadId+":"+start14); + IDeviceAction finalAction = action; + sxsbThreadPool.submit(new Runnable() { + @Override + public void run() { + thingService.post(pluginInfo.getPluginId(), finalAction); + } + }); + long start15= System.currentTimeMillis() ; + log.info("threadId=450{}",threadId+":"+start15); } catch (Exception e) { log.error("message is illegal.", e); } @@ -429,17 +484,22 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { return; }*/ - //上线 - thingService.post( - pluginInfo.getPluginId(), - fillAction(DeviceStateChange.builder() - .productKey(pk) - .deviceName(dn) - .state(DeviceState.ONLINE) - .build() - ) - ); - DEVICE_ONLINE.put(dn, true); + onlineThreadPool.submit(new Runnable() { + @Override + public void run() { + //上线 + 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) { @@ -502,7 +562,7 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { // System.out.println("BCD Time: " + formatBCDToHex(generateBCDTime())); String replyMessage = buildFrame("01", "01", mid, cmd, "00" + formatBCDToHex(generateBCDTime())); - return replyMessage; + return replyMessage; } public static String formatHexByte(String input) { // 移除0x前缀并保留纯十六进制字符 @@ -515,8 +575,8 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { } /** - * 回复设备 - */ + * 回复设备 + */ private void reply1(String replyMessage,String deviceName, JsonObject payload) { Map payloadReply = new HashMap<>(); if(ObjectUtil.isNull(replyMessage)) { @@ -543,7 +603,7 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { System.out.println("cmd" + cmd); // System.out.println(mid + "re++++++++++++++++++++++++++++++++++++++++++++++++++" + cmd); // System.out.println("BCD Time: " + formatBCDToHex(generateBCDTime())); - replyMessage = buildFrame("01", "01", + replyMessage = buildFrame("01", "01", mid, cmd, "00" + formatBCDToHex(generateBCDTime())); } payloadReply.put("data", replyMessage); @@ -565,11 +625,11 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { 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", + // System.out.println("BCD Time: " + formatBCDToHex(generateBCDTime())); + String replyMessage = buildFrame("01","01", "00","01","01"+formatBCDToHex(generateBCDTime())); - if( StringUtils.isBlank(payload.getString("method"))){ + if( StringUtils.isBlank(payload.getString("method"))){ payloadReply.put("id", payload.getString("messageID")); payloadReply.put("method", "thing.event.property.post_reply"); payloadReply.put("code", code); @@ -578,25 +638,25 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { 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 = 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 = 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"); + // 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); - } + 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")); @@ -623,7 +683,7 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { public Map getConfigMap(JsonObject payload) { JsonArray attribute = new JsonArray(); attribute.add(payload.getMap().get("params")); - // JsonArray attribute = payload.getJsonArray("params"); + // JsonArray attribute = payload.getJsonArray("params"); List points = attribute.getList(); Map attrMap = (Map) points.get(0); List>arrList = (List>) attrMap.get("points"); @@ -678,3 +738,4 @@ public class EmqxPlugin implements PluginCloseListener, IPlugin, Runnable { return null; } } + diff --git a/emqx-plugin/src/main/resources/application.yml b/emqx-plugin/src/main/resources/application.yml index ca8141e..95d9200 100644 --- a/emqx-plugin/src/main/resources/application.yml +++ b/emqx-plugin/src/main/resources/application.yml @@ -1,5 +1,5 @@ plugin: - runMode: prod + runMode: dev mainPackage: cc.iotkit.plugin emqx: diff --git a/pom.xml b/pom.xml index b486840..4343be7 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 2.7.11 - 2.10.18 + 2.10.30 cc.iotkit.plugins iot-iita-plugins @@ -93,4 +93,4 @@ - \ No newline at end of file + From c9c45cbdf077ce97d7cac3d1748965be977742f4 Mon Sep 17 00:00:00 2001 From: alsszer Date: Tue, 28 Jul 2026 15:25:58 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat(device):=20=E6=96=B0=E5=A2=9E=E6=89=B9?= =?UTF-8?q?=E9=87=8FPDF=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=9C=8D=E5=8A=A1=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增BatchExportRequest和BatchPdfWrapper数据传输对象 - 实现DeviceInstallInfoController中的getPdfBatch接口支持批量PDF下载 - 添加ExportTask类用于管理导出任务状态 - 配置PDFGenerator工具类的批量处理功能 - 注入ExecutorService线程池支持异步任务执行 - 更新application-dev.yml中的数据库、ES和Redis连接地址 - 调整Hibernate SQL日志输出配置 - 优化PDF生成过程中的压缩和文件名处理 - 在告警消息中增加设备位置信息 --- .../data/dao/DiccidRecordRepository.java | 2 +- .../data/service/DIccidRecordDataImpl.java | 2 +- .../iotkit/manager/config/PDFGenerator.java | 45 ++++++----- .../deviceinfo/DeviceInstallInfoExpordVo.java | 13 ++-- .../vo/deviceinfo/DeviceInstallInfoVo.java | 13 ++-- .../impl/DeviceInstallInfoServiceImpl.java | 5 +- .../handler/RuleDeviceConsumer.java | 74 ++++++++++++------- 7 files changed, 94 insertions(+), 60 deletions(-) diff --git a/iot-dao/iot-data-serviceImpl-rdb/src/main/java/cc/iotkit/data/dao/DiccidRecordRepository.java b/iot-dao/iot-data-serviceImpl-rdb/src/main/java/cc/iotkit/data/dao/DiccidRecordRepository.java index 8ebd9c5..1e6aed9 100644 --- a/iot-dao/iot-data-serviceImpl-rdb/src/main/java/cc/iotkit/data/dao/DiccidRecordRepository.java +++ b/iot-dao/iot-data-serviceImpl-rdb/src/main/java/cc/iotkit/data/dao/DiccidRecordRepository.java @@ -38,5 +38,5 @@ public interface DiccidRecordRepository extends JpaRepository findAll(); - TbDIccidRecordDO findByImei(String imei); + TbDIccidRecordDO findFirstByImei(String imei); } diff --git a/iot-dao/iot-data-serviceImpl-rdb/src/main/java/cc/iotkit/data/service/DIccidRecordDataImpl.java b/iot-dao/iot-data-serviceImpl-rdb/src/main/java/cc/iotkit/data/service/DIccidRecordDataImpl.java index 3e738fd..b39f466 100644 --- a/iot-dao/iot-data-serviceImpl-rdb/src/main/java/cc/iotkit/data/service/DIccidRecordDataImpl.java +++ b/iot-dao/iot-data-serviceImpl-rdb/src/main/java/cc/iotkit/data/service/DIccidRecordDataImpl.java @@ -77,7 +77,7 @@ public class DIccidRecordDataImpl implements IDIccidRecordData, IJPACommData imageCache = new ConcurrentHashMap<>(); - // 线程池用于并发处理图片 private static final ExecutorService imageProcessingPool = Executors.newFixedThreadPool(4); @@ -87,7 +87,7 @@ public class PDFGenerator { {"报警器编号", entity.getDeviceName()}, {"切断阀编号", entity.getShutValueNumber()}, {"燃气表号", entity.getGasMeterNumber()}, - {"备注", entity.getBuildingUnit()} + {"备注", entity.getRemarks()} }; drawTable(cs, font, TABLE_MARGIN, currentY, baseData, COLUMN_WIDTHS); currentY -= (ROW_HEIGHT * baseData.length) + 20f; @@ -396,25 +396,34 @@ public class PDFGenerator { return PDType0Font.load(document, new File("/ttf/NotoSansCJK-Regular.ttf")); } - // 缓存已处理的图片以避免重复下载和处理 - private static final ConcurrentHashMap imageByteCache = new ConcurrentHashMap<>(); - + // LRU缓存图片字节,最多50张,30分钟未访问自动淘汰 + private static final Cache imageByteCache = CacheBuilder.newBuilder() + .maximumSize(50) + .expireAfterAccess(30, TimeUnit.MINUTES) + .build(); // 预加载图片以避免重复下载 private static PDImageXObject loadImageFromUrl(PDDocument document, String url) throws IOException { - byte[] imageBytes = imageByteCache.computeIfAbsent(url, key -> { - try { - return new URL(key).openStream().readAllBytes(); - } catch (IOException e) { - return null; // 或者抛出异常 + try { + byte[] imageBytes = imageByteCache.get(url, () -> { + try { + return new URL(url).openStream().readAllBytes(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + if (imageBytes == null) { + return null; } - }); - if (imageBytes == null) { - return null; + return PDImageXObject.createFromByteArray(document, imageBytes, ""); + } catch (Exception e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } + throw new IOException("Failed to load image: " + url, e); } - - return PDImageXObject.createFromByteArray(document, imageBytes, ""); } diff --git a/iot-module/iot-manager/src/main/java/cc/iotkit/manager/dto/vo/deviceinfo/DeviceInstallInfoExpordVo.java b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/dto/vo/deviceinfo/DeviceInstallInfoExpordVo.java index 057cf5e..7595bb3 100644 --- a/iot-module/iot-manager/src/main/java/cc/iotkit/manager/dto/vo/deviceinfo/DeviceInstallInfoExpordVo.java +++ b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/dto/vo/deviceinfo/DeviceInstallInfoExpordVo.java @@ -58,9 +58,12 @@ public class DeviceInstallInfoExpordVo implements Serializable { // @ApiModelProperty(value = "用户电话") @ExcelProperty(value = "电话") private String userIpone; - // @ApiModelProperty(value = "切断阀编号") - @ExcelProperty(value = "地址") - private String site; + @ExcelProperty(value = "小区名称") + private String communityName; + @ExcelProperty(value = "楼栋单元号") + private String buildingUnit; + @ExcelProperty(value = "房间号") + private String roomNo; @ExcelProperty(value = "切断阀编号") private String shutValueNumber; @ExcelProperty(value = "报警器编号") @@ -79,8 +82,6 @@ public class DeviceInstallInfoExpordVo implements Serializable { private String manufacturer; @ExcelProperty(value = "安装情况") private String position; - /* // @ApiModelProperty(value = "备注") + @ExcelProperty(value = "备注") private String remarks; - - private Integer state;*/ } diff --git a/iot-module/iot-manager/src/main/java/cc/iotkit/manager/dto/vo/deviceinfo/DeviceInstallInfoVo.java b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/dto/vo/deviceinfo/DeviceInstallInfoVo.java index e12d8f0..68ab9cc 100644 --- a/iot-module/iot-manager/src/main/java/cc/iotkit/manager/dto/vo/deviceinfo/DeviceInstallInfoVo.java +++ b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/dto/vo/deviceinfo/DeviceInstallInfoVo.java @@ -61,9 +61,12 @@ public class DeviceInstallInfoVo implements Serializable { // @ApiModelProperty(value = "用户电话") @ExcelProperty(value = "电话") private String userIpone; - // @ApiModelProperty(value = "切断阀编号") - @ExcelProperty(value = "地址") - private String site; + @ExcelProperty(value = "小区名称") + private String communityName; + @ExcelProperty(value = "楼栋单元号") + private String buildingUnit; + @ExcelProperty(value = "房间号") + private String roomNo; @ExcelProperty(value = "切断阀编号") private String shutValueNumber; @ExcelProperty(value = "报警器编号") @@ -82,8 +85,6 @@ public class DeviceInstallInfoVo implements Serializable { private String manufacturer; @ExcelProperty(value = "安装情况") private String position; - /* // @ApiModelProperty(value = "备注") + @ExcelProperty(value = "备注") private String remarks; - - private Integer state;*/ } diff --git a/iot-module/iot-manager/src/main/java/cc/iotkit/manager/service/impl/DeviceInstallInfoServiceImpl.java b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/service/impl/DeviceInstallInfoServiceImpl.java index 5a1c764..b5bb82a 100644 --- a/iot-module/iot-manager/src/main/java/cc/iotkit/manager/service/impl/DeviceInstallInfoServiceImpl.java +++ b/iot-module/iot-manager/src/main/java/cc/iotkit/manager/service/impl/DeviceInstallInfoServiceImpl.java @@ -225,7 +225,10 @@ public class DeviceInstallInfoServiceImpl implements IDeviceInstallInfoService { deviceInstallInfoVo.setXuhao(i); deviceInstallInfoVo.setShutValueNumber(deviceInfo.getShutValueNumber()); deviceInstallInfoVo.setDeviceName(deviceInfo.getDeviceName()); - deviceInstallInfoVo.setSite(tbDeviceInfo.getCommunityName() + tbDeviceInfo.getBuildingUnit() + tbDeviceInfo.getRoomNo()); + deviceInstallInfoVo.setCommunityName(tbDeviceInfo.getCommunityName()); + deviceInstallInfoVo.setBuildingUnit(tbDeviceInfo.getBuildingUnit()); + deviceInstallInfoVo.setRoomNo(tbDeviceInfo.getRoomNo()); + deviceInstallInfoVo.setRemarks(tbDeviceInfo.getRemarks()); deviceInstallInfoVo.setManufacturer("天津费加罗"); deviceInstallInfoVo.setPosition("成功"); diff --git a/iot-module/iot-rule-engine/src/main/java/cc/iotkit/ruleengine/handler/RuleDeviceConsumer.java b/iot-module/iot-rule-engine/src/main/java/cc/iotkit/ruleengine/handler/RuleDeviceConsumer.java index 7c5676a..85f18dc 100644 --- a/iot-module/iot-rule-engine/src/main/java/cc/iotkit/ruleengine/handler/RuleDeviceConsumer.java +++ b/iot-module/iot-rule-engine/src/main/java/cc/iotkit/ruleengine/handler/RuleDeviceConsumer.java @@ -25,69 +25,65 @@ package cc.iotkit.ruleengine.handler; import cc.iotkit.common.constant.Constants; import cc.iotkit.common.thing.ThingModelMessage; -import cc.iotkit.common.utils.JsonUtils; -import cc.iotkit.common.utils.ThreadUtil; import cc.iotkit.mq.ConsumerHandler; import cc.iotkit.mq.MqConsumer; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; @Slf4j public class RuleDeviceConsumer implements ConsumerHandler, ApplicationContextAware { private final List handlers = new ArrayList<>(); - private ScheduledThreadPoolExecutor messageHandlerPool; + private ThreadPoolExecutor messageHandlerPool; + + /** 连续失败计数器 */ + private final AtomicInteger consecutiveFailures = new AtomicInteger(0); + /** 熔断打开截止时间戳,0 表示关闭 */ + private volatile long circuitOpenUntil = 0; + + private static final int FAILURE_THRESHOLD = 10; + private static final long CIRCUIT_COOLDOWN_MS = 30_000; + private static final int QUEUE_CAPACITY = 500; - //private ThreadPoolExecutor messageHandlerPool; @SneakyThrows public RuleDeviceConsumer(MqConsumer consumer) { consumer.consume(Constants.THING_MODEL_MESSAGE_TOPIC, this); } - /* @Override + + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { Map handlerMap = applicationContext.getBeansOfType(DeviceMessageHandler.class); - - // 动态计算核心线程数 - int corePoolSize = Math.max(4, Runtime.getRuntime().availableProcessors()); - // corePoolSize = corePoolSize > handlerMap.size()?corePoolSize:handlerMap.size(); - int maxPoolSize = corePoolSize * 2; - + int coreSize = handlerMap.size(); messageHandlerPool = new ThreadPoolExecutor( - corePoolSize, - maxPoolSize, + coreSize, + coreSize * 2, 60L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(10), // 限制队列大小 - new ThreadFactoryBuilder().setNameFormat("msg-handler-%d").build(), - new ThreadPoolExecutor.CallerRunsPolicy() // 饱和策略 + new LinkedBlockingQueue<>(QUEUE_CAPACITY), + new ThreadPoolExecutor.CallerRunsPolicy() ); - - this.handlers.addAll(handlerMap.values()); - }*/ - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - Map handlerMap = applicationContext.getBeansOfType(DeviceMessageHandler.class); - messageHandlerPool = ThreadUtil.newScheduled(handlerMap.size() * 2, "messageHandler"); this.handlers.addAll(handlerMap.values()); } @SneakyThrows @Override public void handler(ThingModelMessage msg) { + if (circuitOpenUntil > 0 && circuitOpenUntil > System.currentTimeMillis()) { + return; + } + log.info("received thing model message:{}", msg); try { for (DeviceMessageHandler handler : this.handlers) { @@ -97,8 +93,17 @@ public class RuleDeviceConsumer implements ConsumerHandler, A msg.setData(new HashMap<>()); } handler.handle(msg); + // 成功 → 重置熔断 + consecutiveFailures.set(0); + circuitOpenUntil = 0; } catch (Throwable e) { log.error("handler message error", e); + int failures = consecutiveFailures.incrementAndGet(); + if (failures >= FAILURE_THRESHOLD) { + circuitOpenUntil = System.currentTimeMillis() + CIRCUIT_COOLDOWN_MS; + log.error("Circuit breaker OPEN for {}s, {} consecutive failures", + CIRCUIT_COOLDOWN_MS / 1000, failures); + } } }); } @@ -107,4 +112,19 @@ public class RuleDeviceConsumer implements ConsumerHandler, A } } + @PreDestroy + public void shutdown() { + if (messageHandlerPool != null) { + messageHandlerPool.shutdown(); + try { + if (!messageHandlerPool.awaitTermination(10, TimeUnit.SECONDS)) { + messageHandlerPool.shutdownNow(); + } + } catch (InterruptedException e) { + messageHandlerPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + }