From 92eda45afd27259952ba6d0d98774772daf8eeb8 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 14 Feb 2026 16:35:48 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=88IoT?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-dependencies/pom.xml | 8 + .../module/iot/enums/DictTypeConstants.java | 4 +- .../module/iot/enums/ErrorCodeConstants.java | 8 + .../iot/enums/rule/IotDataSinkTypeEnum.java | 4 +- .../rule/IotSceneRuleActionTypeEnum.java | 4 +- .../iot/core/biz/IotDeviceCommonApi.java | 13 +- .../iot/core/biz/dto/IotDeviceRespDTO.java | 8 +- .../dto/IotModbusDeviceConfigListReqDTO.java | 37 + .../biz/dto/IotModbusDeviceConfigRespDTO.java | 66 ++ .../core/biz/dto/IotModbusPointRespDTO.java | 67 ++ .../enums/IotDeviceMessageMethodEnum.java | 2 +- .../iot/core/enums/IotProtocolTypeEnum.java | 47 ++ .../iot/core/enums/IotSerializeTypeEnum.java | 40 ++ .../{ => device}/IotDeviceStateEnum.java | 2 +- .../enums/modbus/IotModbusByteOrderEnum.java | 54 ++ .../modbus/IotModbusFrameFormatEnum.java | 35 + .../core/enums/modbus/IotModbusModeEnum.java | 39 ++ .../modbus/IotModbusRawDataTypeEnum.java | 56 ++ .../config/IotMessageBusProperties.java | 4 +- .../core/messagebus/core/IotMessageBus.java | 10 + .../messagebus/core/IotMessageSubscriber.java | 12 + .../iot/core/mq/message/IotDeviceMessage.java | 39 +- .../topic/auth/IotDeviceRegisterReqDTO.java | 13 +- .../topic/auth/IotDeviceRegisterRespDTO.java | 3 +- .../auth/IotSubDeviceRegisterReqDTO.java | 5 +- .../auth/IotSubDeviceRegisterRespDTO.java | 3 +- .../config/IotDeviceConfigPushReqDTO.java | 54 ++ .../topic/event/IotDeviceEventPostReqDTO.java | 3 +- .../topic/ota/IotDeviceOtaProgressReqDTO.java | 41 ++ .../topic/ota/IotDeviceOtaUpgradeReqDTO.java | 46 ++ .../IotDevicePropertyPackPostReqDTO.java | 3 +- .../property/IotDevicePropertyPostReqDTO.java | 4 +- .../property/IotDevicePropertySetReqDTO.java | 37 + .../service/IotDeviceServiceInvokeReqDTO.java | 32 + .../state/IotDeviceStateUpdateReqDTO.java | 25 + .../topic/topo/IotDeviceTopoAddReqDTO.java | 3 +- .../topic/topo/IotDeviceTopoChangeReqDTO.java | 3 +- .../topic/topo/IotDeviceTopoDeleteReqDTO.java | 3 +- .../topic/topo/IotDeviceTopoGetReqDTO.java | 3 +- .../topic/topo/IotDeviceTopoGetRespDTO.java | 3 +- .../iot/core/util/IotDeviceAuthUtils.java | 8 + .../iot/core/util/IotProductAuthUtils.java | 55 ++ .../yudao-module-iot-gateway/pom.xml | 8 +- .../gateway/codec/IotDeviceMessageCodec.java | 33 - .../alink/IotAlinkDeviceMessageCodec.java | 89 --- .../iot/gateway/codec/package-info.java | 4 - .../gateway/codec/simple/package-info.java | 4 - .../tcp/IotTcpJsonDeviceMessageCodec.java | 110 --- .../config/IotGatewayConfiguration.java | 252 +------ .../gateway/config/IotGatewayProperties.java | 637 +++--------------- ...tractIotProtocolDownstreamSubscriber.java} | 57 +- .../iot/gateway/protocol/IotProtocol.java | 52 ++ .../gateway/protocol/IotProtocolManager.java | 217 ++++++ .../gateway/protocol/coap/IotCoapConfig.java | 36 + .../coap/IotCoapDownstreamSubscriber.java | 46 -- .../protocol/coap/IotCoapProtocol.java | 168 +++++ .../coap/IotCoapUpstreamProtocol.java | 90 --- .../IotCoapDownstreamSubscriber.java | 27 + .../upstream/IotCoapAbstractHandler.java | 186 +++++ .../handler/upstream/IotCoapAuthHandler.java | 72 ++ .../upstream}/IotCoapAuthResource.java | 10 +- .../upstream/IotCoapRegisterHandler.java | 46 ++ .../upstream}/IotCoapRegisterResource.java | 2 +- .../upstream/IotCoapRegisterSubHandler.java | 84 +++ .../upstream/IotCoapRegisterSubResource.java | 52 ++ .../upstream/IotCoapUpstreamHandler.java | 76 +++ .../IotCoapUpstreamTopicResource.java | 21 +- .../gateway/protocol/coap/package-info.java | 7 - .../coap/router/IotCoapAuthHandler.java | 117 ---- .../coap/router/IotCoapRegisterHandler.java | 98 --- .../coap/router/IotCoapUpstreamHandler.java | 110 --- .../protocol/coap/util/IotCoapUtils.java | 84 --- .../emqx/IotEmqxAuthEventProtocol.java | 104 --- .../gateway/protocol/emqx/IotEmqxConfig.java | 225 +++++++ .../protocol/emqx/IotEmqxProtocol.java | 532 +++++++++++++++ .../emqx/IotEmqxUpstreamProtocol.java | 365 ---------- .../downstream}/IotEmqxDownstreamHandler.java | 15 +- .../IotEmqxDownstreamSubscriber.java | 29 + .../upstream}/IotEmqxAuthEventHandler.java | 376 ++++++++--- .../upstream}/IotEmqxUpstreamHandler.java | 28 +- .../gateway/protocol/http/IotHttpConfig.java | 13 + .../http/IotHttpDownstreamSubscriber.java | 45 -- .../protocol/http/IotHttpProtocol.java | 176 +++++ .../http/IotHttpUpstreamProtocol.java | 91 --- .../IotHttpDownstreamSubscriber.java | 27 + .../upstream}/IotHttpAbstractHandler.java | 42 +- .../upstream}/IotHttpAuthHandler.java | 44 +- .../upstream}/IotHttpRegisterHandler.java | 31 +- .../upstream}/IotHttpRegisterSubHandler.java | 40 +- .../upstream}/IotHttpUpstreamHandler.java | 34 +- .../AbstractIotModbusPollScheduler.java | 278 ++++++++ .../common/utils/IotModbusCommonUtils.java | 557 +++++++++++++++ .../common/utils/IotModbusTcpClientUtils.java | 195 ++++++ .../tcpclient/IotModbusTcpClientConfig.java | 22 + .../tcpclient/IotModbusTcpClientProtocol.java | 218 ++++++ .../IotModbusTcpClientDownstreamHandler.java | 107 +++ ...otModbusTcpClientDownstreamSubscriber.java | 31 + .../IotModbusTcpClientUpstreamHandler.java | 60 ++ .../IotModbusTcpClientConfigCacheService.java | 104 +++ .../IotModbusTcpClientConnectionManager.java | 317 +++++++++ .../IotModbusTcpClientPollScheduler.java | 73 ++ .../modbus/tcpclient/package-info.java | 6 + .../tcpserver/IotModbusTcpServerConfig.java | 44 ++ .../tcpserver/IotModbusTcpServerProtocol.java | 334 +++++++++ .../tcpserver/codec/IotModbusFrame.java | 57 ++ .../codec/IotModbusFrameDecoder.java | 477 +++++++++++++ .../codec/IotModbusFrameEncoder.java | 210 ++++++ .../IotModbusTcpServerDownstreamHandler.java | 152 +++++ ...otModbusTcpServerDownstreamSubscriber.java | 31 + .../IotModbusTcpServerUpstreamHandler.java | 280 ++++++++ .../IotModbusTcpServerConfigCacheService.java | 118 ++++ .../IotModbusTcpServerConnectionManager.java | 174 +++++ ...tModbusTcpServerPendingRequestManager.java | 154 +++++ .../IotModbusTcpServerPollScheduler.java | 111 +++ .../modbus/tcpserver/package-info.java | 6 + .../gateway/protocol/mqtt/IotMqttConfig.java | 29 + .../mqtt/IotMqttDownstreamSubscriber.java | 79 --- .../protocol/mqtt/IotMqttProtocol.java | 344 ++++++++++ .../mqtt/IotMqttUpstreamProtocol.java | 92 --- .../downstream/IotMqttDownstreamHandler.java | 70 ++ .../IotMqttDownstreamSubscriber.java | 31 + .../upstream/IotMqttAbstractHandler.java | 86 +++ .../handler/upstream/IotMqttAuthHandler.java | 119 ++++ .../upstream/IotMqttRegisterHandler.java | 89 +++ .../upstream/IotMqttUpstreamHandler.java | 79 +++ .../manager/IotMqttConnectionManager.java | 58 +- .../mqtt/router/IotMqttDownstreamHandler.java | 132 ---- .../mqtt/router/IotMqttUpstreamHandler.java | 511 -------------- .../iot/gateway/protocol/package-info.java | 4 +- .../gateway/protocol/tcp/IotTcpConfig.java | 91 +++ .../tcp/IotTcpDownstreamSubscriber.java | 64 -- .../gateway/protocol/tcp/IotTcpProtocol.java | 207 ++++++ .../protocol/tcp/IotTcpUpstreamProtocol.java | 99 --- .../tcp/codec/IotTcpCodecTypeEnum.java | 54 ++ .../protocol/tcp/codec/IotTcpFrameCodec.java | 43 ++ .../tcp/codec/IotTcpFrameCodecFactory.java | 27 + .../delimiter/IotTcpDelimiterFrameCodec.java | 89 +++ .../length/IotTcpFixedLengthFrameCodec.java | 64 ++ .../length/IotTcpLengthFieldFrameCodec.java | 181 +++++ .../downstream/IotTcpDownstreamHandler.java | 74 ++ .../IotTcpDownstreamSubscriber.java | 31 + .../upstream/IotTcpUpstreamHandler.java | 328 +++++++++ .../tcp/manager/IotTcpConnectionManager.java | 68 +- .../tcp/router/IotTcpDownstreamHandler.java | 54 -- .../tcp/router/IotTcpUpstreamHandler.java | 508 -------------- .../gateway/protocol/udp/IotUdpConfig.java | 43 ++ .../udp/IotUdpDownstreamSubscriber.java | 64 -- .../gateway/protocol/udp/IotUdpProtocol.java | 188 ++++++ .../protocol/udp/IotUdpUpstreamProtocol.java | 171 ----- .../downstream/IotUdpDownstreamHandler.java | 65 ++ .../IotUdpDownstreamSubscriber.java | 31 + .../upstream/IotUdpUpstreamHandler.java | 376 +++++++++++ .../udp/manager/IotUdpSessionManager.java | 189 +++--- .../udp/router/IotUdpDownstreamHandler.java | 70 -- .../udp/router/IotUdpUpstreamHandler.java | 542 --------------- .../websocket/IotWebSocketConfig.java | 38 ++ .../IotWebSocketDownstreamSubscriber.java | 64 -- .../websocket/IotWebSocketProtocol.java | 208 ++++++ .../IotWebSocketUpstreamProtocol.java | 110 --- .../IotWebSocketDownstreamHandler.java | 26 +- .../IotWebSocketDownstreamSubscriber.java | 31 + .../upstream/IotWebSocketUpstreamHandler.java | 305 +++++++++ .../IotWebSocketConnectionManager.java | 34 +- .../router/IotWebSocketUpstreamHandler.java | 471 ------------- .../serialize/IotMessageSerializer.java | 38 ++ .../IotMessageSerializerManager.java | 60 ++ .../binary/IotBinarySerializer.java} | 86 +-- .../serialize/json/IotJsonSerializer.java | 37 + .../iot/gateway/serialize/package-info.java | 6 + .../message/IotDeviceMessageService.java | 39 +- .../message/IotDeviceMessageServiceImpl.java | 80 +-- .../device/remote/IotDeviceApiImpl.java | 16 +- .../iot/gateway/util/IotMqttTopicUtils.java | 80 ++- .../src/main/resources/application.yaml | 204 +++--- ...rectDeviceCoapProtocolIntegrationTest.java | 19 +- ...ewayDeviceCoapProtocolIntegrationTest.java | 13 +- ...ySubDeviceCoapProtocolIntegrationTest.java | 7 +- .../gateway/protocol/emqx/package-info.java | 18 + ...rectDeviceHttpProtocolIntegrationTest.java | 17 +- ...ewayDeviceHttpProtocolIntegrationTest.java | 34 +- ...ySubDeviceHttpProtocolIntegrationTest.java | 5 - .../protocol/modbus/ModbusRtuOverTcpDemo.java | 304 +++++++++ .../IoTModbusTcpClientIntegrationTest.java | 114 ++++ .../IotModbusTcpServerRtuIntegrationTest.java | 302 +++++++++ .../IotModbusTcpServerTcpIntegrationTest.java | 302 +++++++++ ...rectDeviceMqttProtocolIntegrationTest.java | 339 ++++------ ...ewayDeviceMqttProtocolIntegrationTest.java | 407 +++++------ ...ySubDeviceMqttProtocolIntegrationTest.java | 219 +++--- ...irectDeviceTcpProtocolIntegrationTest.java | 288 ++++---- ...tewayDeviceTcpProtocolIntegrationTest.java | 354 +++++----- ...aySubDeviceTcpProtocolIntegrationTest.java | 243 ++++--- ...irectDeviceUdpProtocolIntegrationTest.java | 183 ++--- ...tewayDeviceUdpProtocolIntegrationTest.java | 231 ++----- ...aySubDeviceUdpProtocolIntegrationTest.java | 137 ++-- ...eviceWebSocketProtocolIntegrationTest.java | 54 +- ...eviceWebSocketProtocolIntegrationTest.java | 60 +- ...eviceWebSocketProtocolIntegrationTest.java | 36 +- .../yudao-module-iot-server/pom.xml | 12 - .../iot/api/device/IoTDeviceApiImpl.java | 72 +- .../IotDeviceModbusConfigController.java | 54 ++ .../IotDeviceModbusPointController.java | 72 ++ .../device/vo/device/IotDevicePageReqVO.java | 2 +- .../vo/message/IotDeviceMessageRespVO.java | 2 +- .../modbus/IotDeviceModbusConfigRespVO.java | 48 ++ .../IotDeviceModbusConfigSaveReqVO.java | 46 ++ .../modbus/IotDeviceModbusPointPageReqVO.java | 30 + .../vo/modbus/IotDeviceModbusPointRespVO.java | 55 ++ .../modbus/IotDeviceModbusPointSaveReqVO.java | 54 ++ .../admin/ota/IotOtaTaskRecordController.java | 22 +- .../admin/product/IotProductController.http | 5 + .../admin/product/IotProductController.java | 8 + .../product/vo/product/IotProductRespVO.java | 17 +- .../vo/product/IotProductSaveReqVO.java | 14 +- .../statistics/IotStatisticsController.java | 2 +- .../iot/core/mq/message/IotDeviceMessage.java | 168 ----- .../iot/core/util/IotDeviceAuthUtils.java | 52 -- .../dal/dataobject/device/IotDeviceDO.java | 11 +- .../dataobject/device/IotDeviceMessageDO.java | 2 +- .../device/IotDeviceModbusConfigDO.java | 85 +++ .../device/IotDeviceModbusPointDO.java | 103 +++ .../dal/dataobject/product/IotProductDO.java | 19 +- .../device/IotDeviceModbusConfigMapper.java | 30 + .../device/IotDeviceModbusPointMapper.java | 47 ++ .../dal/mysql/product/IotProductMapper.java | 4 + .../job/device/IotDeviceOfflineCheckJob.java | 2 +- .../module/iot/job/ota/IotOtaUpgradeJob.java | 2 +- .../device/IotDeviceMessageSubscriber.java | 3 +- ...java => IotDataRuleMessageSubscriber.java} | 2 +- ...ava => IotSceneRuleMessageSubscriber.java} | 3 +- .../alert/IotAlertConfigServiceImpl.java | 4 +- .../device/IotDeviceModbusConfigService.java | 48 ++ .../IotDeviceModbusConfigServiceImpl.java | 89 +++ .../device/IotDeviceModbusPointService.java | 75 +++ .../IotDeviceModbusPointServiceImpl.java | 135 ++++ .../iot/service/device/IotDeviceService.java | 2 +- .../service/device/IotDeviceServiceImpl.java | 8 +- .../ota/IotOtaTaskRecordServiceImpl.java | 23 +- .../service/product/IotProductService.java | 28 + .../product/IotProductServiceImpl.java | 36 +- .../rule/data/IotDataSinkServiceImpl.java | 4 +- .../action/IotDataRuleCacheableAction.java | 2 - .../data/action/IotKafkaDataRuleAction.java | 2 +- ... IotDevicePropertySetSceneRuleAction.java} | 3 +- ...IotDeviceServiceInvokeSceneRuleAction.java | 2 - .../thingmodel/IotThingModelServiceImpl.java | 10 +- 245 files changed, 14927 insertions(+), 7689 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java rename yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/{ => device}/IotDeviceStateEnum.java (94%) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusByteOrderEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusFrameFormatEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusModeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusRawDataTypeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/{emqx/IotEmqxDownstreamSubscriber.java => AbstractIotProtocolDownstreamSubscriber.java} (57%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/{router => handler/upstream}/IotCoapAuthResource.java (64%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/{router => handler/upstream}/IotCoapRegisterResource.java (92%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/{router => handler/upstream}/IotCoapUpstreamTopicResource.java (70%) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/{router => handler/downstream}/IotEmqxDownstreamHandler.java (88%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/{router => handler/upstream}/IotEmqxAuthEventHandler.java (52%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/{router => handler/upstream}/IotEmqxUpstreamHandler.java (61%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpAbstractHandler.java (72%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpAuthHandler.java (66%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpRegisterHandler.java (54%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpRegisterSubHandler.java (61%) rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/{router => handler/upstream}/IotHttpUpstreamHandler.java (62%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/{router => handler/downstream}/IotWebSocketDownstreamHandler.java (58%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java rename yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/{codec/tcp/IotTcpBinaryDeviceMessageCodec.java => serialize/binary/IotBinarySerializer.java} (81%) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http delete mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java delete mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java rename yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/{IotDataRuleMessageHandler.java => IotDataRuleMessageSubscriber.java} (93%) rename yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/{IotSceneRuleMessageHandler.java => IotSceneRuleMessageSubscriber.java} (90%) create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java create mode 100644 yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java rename yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/{IotDeviceControlSceneRuleAction.java => IotDevicePropertySetSceneRuleAction.java} (98%) diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 8becccb5f..ddfe194b3 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -77,6 +77,7 @@ 4.5.22 4.12.0 3.12.0 + 3.2.1 2.40.15 1.16.7 @@ -656,6 +657,13 @@ ${californium.version} + + + com.ghgande + j2mod + ${j2mod.version} + + software.amazon.awssdk diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java index 4f07ddfc1..cf6bec118 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java @@ -8,8 +8,8 @@ package cn.iocoder.yudao.module.iot.enums; public class DictTypeConstants { public static final String NET_TYPE = "iot_net_type"; - public static final String LOCATION_TYPE = "iot_location_type"; - public static final String CODEC_TYPE = "iot_codec_type"; + public static final String PROTOCOL_TYPE = "iot_protocol_type"; + public static final String SERIALIZE_TYPE = "iot_serialize_type"; public static final String PRODUCT_STATUS = "iot_product_status"; public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index 3679dbf1c..065eb2d22 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -54,6 +54,14 @@ public interface ErrorCodeConstants { ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); + // ========== 设备 Modbus 配置 1-050-006-000 ========== + ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在"); + ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在"); + + // ========== 设备 Modbus 点位 1-050-007-000 ========== + ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在"); + ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在"); + // ========== OTA 固件相关 1-050-008-000 ========== ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java index 440fab5f5..96b477d69 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java @@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable { TCP(2, "TCP"), WEBSOCKET(3, "WebSocket"), - MQTT(10, "MQTT"), // TODO 待实现; + MQTT(10, "MQTT"), // TODO @puhui999:待实现; - DATABASE(20, "Database"), // TODO @puhui999:待实现;可以简单点,对应的表名是什么,字段先固定了。 + DATABASE(20, "Database"), // TODO @puhui999:待实现; REDIS(21, "Redis"), ROCKETMQ(30, "RocketMQ"), diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleActionTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleActionTypeEnum.java index ad3b4cf17..11c600cb7 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleActionTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleActionTypeEnum.java @@ -18,13 +18,13 @@ public enum IotSceneRuleActionTypeEnum implements ArrayValuable { /** * 设备属性设置 * - * 对应 {@link IotDeviceMessageMethodEnum#PROPERTY_SET} + * 对应 {@link cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum#PROPERTY_SET} */ DEVICE_PROPERTY_SET(1), /** * 设备服务调用 * - * 对应 {@link IotDeviceMessageMethodEnum#SERVICE_INVOKE} + * 对应 {@link cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum#SERVICE_INVOKE} */ DEVICE_SERVICE_INVOKE(2), diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java index cc0cb071a..c0b3f9df3 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java @@ -1,10 +1,7 @@ package cn.iocoder.yudao.module.iot.core.biz; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.*; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; @@ -50,4 +47,12 @@ public interface IotDeviceCommonApi { */ CommonResult> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO); + /** + * 获取 Modbus 设备配置列表 + * + * @param listReqDTO 查询参数 + * @return Modbus 设备配置列表 + */ + CommonResult> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO); + } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java index add116780..8ad2c5bcd 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java @@ -34,8 +34,12 @@ public class IotDeviceRespDTO { */ private Long productId; /** - * 编解码器类型 + * 协议类型 */ - private String codecType; + private String protocolType; + /** + * 序列化类型 + */ + private String serializeType; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java new file mode 100644 index 000000000..7865a09f0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.Set; + +/** + * IoT Modbus 设备配置列表查询 Request DTO + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class IotModbusDeviceConfigListReqDTO { + + /** + * 状态 + */ + private Integer status; + + /** + * 模式 + */ + private Integer mode; + + /** + * 协议类型 + */ + private String protocolType; + + /** + * 设备 ID 集合 + */ + private Set deviceIds; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java new file mode 100644 index 000000000..683bcef4c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import lombok.Data; + +import java.util.List; + +/** + * IoT Modbus 设备配置 Response DTO + * + * @author 芋道源码 + */ +@Data +public class IotModbusDeviceConfigRespDTO { + + /** + * 设备编号 + */ + private Long deviceId; + /** + * 产品标识 + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + + // ========== Modbus 连接配置 ========== + + /** + * Modbus 服务器 IP 地址 + */ + private String ip; + /** + * Modbus 服务器端口 + */ + private Integer port; + /** + * 从站地址 + */ + private Integer slaveId; + /** + * 连接超时时间,单位:毫秒 + */ + private Integer timeout; + /** + * 重试间隔,单位:毫秒 + */ + private Integer retryInterval; + /** + * 模式 + */ + private Integer mode; + /** + * 数据帧格式 + */ + private Integer frameFormat; + + // ========== Modbus 点位配置 ========== + + /** + * 点位列表 + */ + private List points; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java new file mode 100644 index 000000000..dd6f9cf37 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * IoT Modbus 点位配置 Response DTO + * + * @author 芋道源码 + */ +@Data +public class IotModbusPointRespDTO { + + /** + * 点位编号 + */ + private Long id; + /** + * 属性标识符(物模型的 identifier) + */ + private String identifier; + /** + * 属性名称(物模型的 name) + */ + private String name; + + // ========== Modbus 协议配置 ========== + + /** + * Modbus 功能码 + * + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器) + */ + private Integer functionCode; + /** + * 寄存器起始地址 + */ + private Integer registerAddress; + /** + * 寄存器数量 + */ + private Integer registerCount; + /** + * 字节序 + * + * 枚举 {@link IotModbusByteOrderEnum} + */ + private String byteOrder; + /** + * 原始数据类型 + * + * 枚举 {@link IotModbusRawDataTypeEnum} + */ + private String rawDataType; + /** + * 缩放因子 + */ + private BigDecimal scale; + /** + * 轮询间隔(毫秒) + */ + private Integer pollInterval; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java index d98003284..3b4495e33 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java @@ -64,7 +64,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable { // ========== OTA 固件 ========== // 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates - OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false), + OTA_UPGRADE("thing.ota.upgrade", "OTA 固件信息推送", false), OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true), ; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java new file mode 100644 index 000000000..753605426 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 协议类型枚举 + * + * 用于定义传输层协议类型 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotProtocolTypeEnum implements ArrayValuable { + + TCP("tcp"), + UDP("udp"), + WEBSOCKET("websocket"), + HTTP("http"), + MQTT("mqtt"), + EMQX("emqx"), + COAP("coap"), + MODBUS_TCP_CLIENT("modbus_tcp_client"), + MODBUS_TCP_SERVER("modbus_tcp_server"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new); + + /** + * 类型 + */ + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + + public static IotProtocolTypeEnum of(String type) { + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java new file mode 100644 index 000000000..0f9400f36 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotSerializeTypeEnum.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 序列化类型枚举 + * + * 用于定义设备消息的序列化格式 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotSerializeTypeEnum implements ArrayValuable { + + JSON("json"), + BINARY("binary"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotSerializeTypeEnum::getType).toArray(String[]::new); + + /** + * 类型 + */ + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + + public static IotSerializeTypeEnum of(String type) { + return ArrayUtil.firstMatch(e -> e.getType().equals(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/device/IotDeviceStateEnum.java similarity index 94% rename from yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/device/IotDeviceStateEnum.java index d0ff8357e..fd8ca0e31 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/device/IotDeviceStateEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.core.enums; +package cn.iocoder.yudao.module.iot.core.enums.device; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusByteOrderEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusByteOrderEnum.java new file mode 100644 index 000000000..229257a17 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusByteOrderEnum.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.core.enums.modbus; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 字节序枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusByteOrderEnum implements ArrayValuable { + + AB("AB", "大端序(16位)", 2), + BA("BA", "小端序(16位)", 2), + ABCD("ABCD", "大端序(32位)", 4), + CDAB("CDAB", "大端字交换(32位)", 4), + DCBA("DCBA", "小端序(32位)", 4), + BADC("BADC", "小端字交换(32位)", 4); + + public static final String[] ARRAYS = Arrays.stream(values()) + .map(IotModbusByteOrderEnum::getOrder) + .toArray(String[]::new); + + /** + * 字节序 + */ + private final String order; + /** + * 名称 + */ + private final String name; + /** + * 字节数 + */ + private final Integer byteCount; + + @Override + public String[] array() { + return ARRAYS; + } + + public static IotModbusByteOrderEnum getByOrder(String order) { + return Arrays.stream(values()) + .filter(e -> e.getOrder().equals(order)) + .findFirst() + .orElse(null); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusFrameFormatEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusFrameFormatEnum.java new file mode 100644 index 000000000..bf1de5414 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusFrameFormatEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.core.enums.modbus; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 数据帧格式枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusFrameFormatEnum implements ArrayValuable { + + MODBUS_TCP(1), + MODBUS_RTU(2); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(IotModbusFrameFormatEnum::getFormat) + .toArray(Integer[]::new); + + /** + * 格式 + */ + private final Integer format; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusModeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusModeEnum.java new file mode 100644 index 000000000..ed4b3891e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusModeEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.core.enums.modbus; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 工作模式枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusModeEnum implements ArrayValuable { + + POLLING(1, "云端轮询"), + ACTIVE_REPORT(2, "边缘采集"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(IotModbusModeEnum::getMode) + .toArray(Integer[]::new); + + /** + * 工作模式 + */ + private final Integer mode; + /** + * 模式名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusRawDataTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusRawDataTypeEnum.java new file mode 100644 index 000000000..522b0aeaf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusRawDataTypeEnum.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.iot.core.enums.modbus; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT Modbus 原始数据类型枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum IotModbusRawDataTypeEnum implements ArrayValuable { + + INT16("INT16", "有符号 16 位整数", 1), + UINT16("UINT16", "无符号 16 位整数", 1), + INT32("INT32", "有符号 32 位整数", 2), + UINT32("UINT32", "无符号 32 位整数", 2), + FLOAT("FLOAT", "32 位浮点数", 2), + DOUBLE("DOUBLE", "64 位浮点数", 4), + BOOLEAN("BOOLEAN", "布尔值(用于线圈)", 1), + STRING("STRING", "字符串", null); // null 表示可变长度 + + public static final String[] ARRAYS = Arrays.stream(values()) + .map(IotModbusRawDataTypeEnum::getType) + .toArray(String[]::new); + + /** + * 数据类型 + */ + private final String type; + /** + * 名称 + */ + private final String name; + /** + * 寄存器数量(null 表示可变) + */ + private final Integer registerCount; + + @Override + public String[] array() { + return ARRAYS; + } + + public static IotModbusRawDataTypeEnum getByType(String type) { + return Arrays.stream(values()) + .filter(e -> e.getType().equals(type)) + .findFirst() + .orElse(null); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java index 501eb2b0d..2273bc5c2 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java @@ -1,12 +1,10 @@ package cn.iocoder.yudao.module.iot.core.messagebus.config; +import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; - /** * IoT 消息总线配置属性 * diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java index c62146761..646eb36bc 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java @@ -24,4 +24,14 @@ public interface IotMessageBus { */ void register(IotMessageSubscriber subscriber); + /** + * 取消注册消息订阅者 + * + * @param subscriber 订阅者 + */ + default void unregister(IotMessageSubscriber subscriber) { + // TODO 芋艿:暂时不实现,需求量不大,但是 + // throw new UnsupportedOperationException("取消注册消息订阅者功能,尚未实现"); + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java index 23a055325..fb5c71239 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java @@ -26,4 +26,16 @@ public interface IotMessageSubscriber { */ void onMessage(T message); + /** + * 启动订阅 + */ + default void start() { + } + + /** + * 停止订阅 + */ + default void stop() { + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java index 6821c0d16..cc9b13874 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java @@ -1,9 +1,9 @@ package cn.iocoder.yudao.module.iot.core.mq.message; -import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.topic.state.IotDeviceStateUpdateReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import lombok.AllArgsConstructor; import lombok.Builder; @@ -60,7 +60,7 @@ public class IotDeviceMessage { */ private String serverId; - // ========== codec(编解码)字段 ========== + // ========== serialize(序列化)相关字段 ========== /** * 请求编号 @@ -72,7 +72,7 @@ public class IotDeviceMessage { * 请求方法 * * 枚举 {@link IotDeviceMessageMethodEnum} - * 例如说:thing.property.report 属性上报 + * 例如说:thing.property.post 属性上报 */ private String method; /** @@ -94,7 +94,7 @@ public class IotDeviceMessage { */ private String msg; - // ========== 基础方法:只传递"codec(编解码)字段" ========== + // ========== 基础方法:只传递"serialize(序列化)相关字段" ========== public static IotDeviceMessage requestOf(String method) { return requestOf(null, method, null); @@ -108,6 +108,23 @@ public class IotDeviceMessage { return of(requestId, method, params, null, null, null); } + /** + * 创建设备请求消息(包含设备信息) + * + * @param deviceId 设备编号 + * @param tenantId 租户编号 + * @param serverId 服务标识 + * @param method 消息方法 + * @param params 消息参数 + * @return 消息对象 + */ + public static IotDeviceMessage requestOf(Long deviceId, Long tenantId, String serverId, + String method, Object params) { + IotDeviceMessage message = of(null, method, params, null, null, null); + return message.setId(IotDeviceMessageUtils.generateMessageId()) + .setDeviceId(deviceId).setTenantId(tenantId).setServerId(serverId); + } + public static IotDeviceMessage replyOf(String requestId, String method, Object data, Integer code, String msg) { if (code == null) { @@ -132,20 +149,12 @@ public class IotDeviceMessage { public static IotDeviceMessage buildStateUpdateOnline() { return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), - MapUtil.of("state", IotDeviceStateEnum.ONLINE.getState())); + new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.ONLINE.getState())); } public static IotDeviceMessage buildStateOffline() { return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), - MapUtil.of("state", IotDeviceStateEnum.OFFLINE.getState())); - } - - public static IotDeviceMessage buildOtaUpgrade(String version, String fileUrl, Long fileSize, - String fileDigestAlgorithm, String fileDigestValue) { - return requestOf(IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), MapUtil.builder() - .put("version", version).put("fileUrl", fileUrl).put("fileSize", fileSize) - .put("fileDigestAlgorithm", fileDigestAlgorithm).put("fileDigestValue", fileDigestValue) - .build()); + new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.OFFLINE.getState())); } } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java index b8db15f18..ad938749d 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java @@ -1,12 +1,15 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import jakarta.validation.constraints.NotEmpty; import lombok.Data; /** * IoT 设备动态注册 Request DTO *

- * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 消息的 params 参数 + *

+ * 直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret * * @author 芋道源码 * @see 阿里云 - 一型一密 @@ -27,9 +30,11 @@ public class IotDeviceRegisterReqDTO { private String deviceName; /** - * 产品密钥 + * 注册签名 + * + * @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String) */ - @NotEmpty(message = "产品密钥不能为空") - private String productSecret; + @NotEmpty(message = "签名不能为空") + private String sign; } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java index 707f79890..681aa72c5 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; /** * IoT 设备动态注册 Response DTO *

- * 用于直连设备/网关的一型一密动态注册响应 + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 响应的设备信息 * * @author 芋道源码 * @see 阿里云 - 一型一密 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java index cf34a1db2..e2372e0cb 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java @@ -1,13 +1,14 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import jakarta.validation.constraints.NotEmpty; import lombok.Data; /** * IoT 子设备动态注册 Request DTO *

- * 用于 thing.auth.register.sub 消息的 params 数组元素 - * + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 消息的 params 数组元素 + *

* 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。 * * @author 芋道源码 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java index a45f14def..7da2f4e47 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor; /** * IoT 子设备动态注册 Response DTO *

- * 用于 thing.auth.register.sub 响应的设备信息 + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 响应的设备信息 * * @author 芋道源码 * @see 阿里云 - 动态注册子设备 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java new file mode 100644 index 000000000..4828c9917 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.core.topic.config; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备配置推送 Request DTO + *

+ * 用于 {@link IotDeviceMessageMethodEnum#CONFIG_PUSH} 下行消息的 params 参数 + * + * @author 芋道源码 + * @see 阿里云 - 远程配置 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceConfigPushReqDTO { + + /** + * 配置编号 + */ + private String configId; + + /** + * 配置文件大小(字节) + */ + private Long configSize; + + /** + * 签名方法 + */ + private String signMethod; + + /** + * 签名 + */ + private String sign; + + /** + * 配置文件下载地址 + */ + private String url; + + /** + * 获取类型 + *

+ * file: 文件 + * content: 内容 + */ + private String getType; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java index 3b6a7a7d4..345419231 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java @@ -1,11 +1,12 @@ package cn.iocoder.yudao.module.iot.core.topic.event; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.Data; /** * IoT 设备事件上报 Request DTO *

- * 用于 thing.event.post 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#EVENT_POST} 消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 设备上报事件 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java new file mode 100644 index 000000000..ef16e3e03 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.iot.core.topic.ota; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备 OTA 升级进度上报 Request DTO + *

+ * 用于 {@link IotDeviceMessageMethodEnum#OTA_PROGRESS} 上行消息的 params 参数 + * + * @author 芋道源码 + * @see 阿里云 - OTA 升级 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceOtaProgressReqDTO { + + /** + * 固件版本号 + */ + private String version; + + /** + * 升级状态 + */ + private Integer status; + + /** + * 描述信息 + */ + private String description; + + /** + * 升级进度(0-100) + */ + private Integer progress; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java new file mode 100644 index 000000000..096ac699b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.core.topic.ota; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备 OTA 固件升级推送 Request DTO + *

+ * 用于 {@link IotDeviceMessageMethodEnum#OTA_UPGRADE} 下行消息的 params 参数 + * + * @author 芋道源码 + * @see 阿里云 - OTA 升级 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceOtaUpgradeReqDTO { + + /** + * 固件版本号 + */ + private String version; + + /** + * 固件文件下载地址 + */ + private String fileUrl; + + /** + * 固件文件大小(字节) + */ + private Long fileSize; + + /** + * 固件文件摘要算法 + */ + private String fileDigestAlgorithm; + + /** + * 固件文件摘要值 + */ + private String fileDigestValue; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java index 24494984e..509e45775 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.property; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import lombok.Data; @@ -9,7 +10,7 @@ import java.util.Map; /** * IoT 设备属性批量上报 Request DTO *

- * 用于 thing.event.property.pack.post 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_PACK_POST} 消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 网关批量上报数据 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java index 2e537442d..98471d1d5 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java @@ -1,12 +1,14 @@ package cn.iocoder.yudao.module.iot.core.topic.property; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; + import java.util.HashMap; import java.util.Map; /** * IoT 设备属性上报 Request DTO *

- * 用于 thing.property.post 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_POST} 消息的 params 参数 *

* 本质是一个 Map,key 为属性标识符,value 为属性值 * diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java new file mode 100644 index 000000000..ba51f1bba --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.core.topic.property; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; + +import java.util.HashMap; +import java.util.Map; + +/** + * IoT 设备属性设置 Request DTO + *

+ * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_SET} 下行消息的 params 参数 + *

+ * 本质是一个 Map,key 为属性标识符,value 为属性值 + * + * @author 芋道源码 + */ +public class IotDevicePropertySetReqDTO extends HashMap { + + public IotDevicePropertySetReqDTO() { + super(); + } + + public IotDevicePropertySetReqDTO(Map properties) { + super(properties); + } + + /** + * 创建属性设置 DTO + * + * @param properties 属性数据 + * @return DTO 对象 + */ + public static IotDevicePropertySetReqDTO of(Map properties) { + return new IotDevicePropertySetReqDTO(properties); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java new file mode 100644 index 000000000..dafadd24a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.iot.core.topic.service; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * IoT 设备服务调用 Request DTO + *

+ * 用于 {@link IotDeviceMessageMethodEnum#SERVICE_INVOKE} 下行消息的 params 参数 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceServiceInvokeReqDTO { + + /** + * 服务标识符 + */ + private String identifier; + + /** + * 服务输入参数 + */ + private Map inputParams; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java new file mode 100644 index 000000000..fce44e03b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.core.topic.state; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备状态更新 Request DTO + *

+ * 用于 {@link IotDeviceMessageMethodEnum#STATE_UPDATE} 消息的 params 参数 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceStateUpdateReqDTO { + + /** + * 设备状态 + */ + private Integer state; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java index 97ec33200..b9444ed6d 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import jakarta.validation.constraints.NotEmpty; import lombok.Data; @@ -9,7 +10,7 @@ import java.util.List; /** * IoT 设备拓扑添加 Request DTO *

- * 用于 thing.topo.add 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_ADD} 消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 添加拓扑关系 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java index 0198206fe..615e509ae 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import lombok.AllArgsConstructor; import lombok.Data; @@ -10,7 +11,7 @@ import java.util.List; /** * IoT 设备拓扑关系变更通知 Request DTO *

- * 用于 thing.topo.change 下行消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_CHANGE} 下行消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 通知网关拓扑关系变化 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java index 71ee2bb8b..6db2b5db8 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; @@ -10,7 +11,7 @@ import java.util.List; /** * IoT 设备拓扑删除 Request DTO *

- * 用于 thing.topo.delete 消息的 params 参数 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_DELETE} 消息的 params 参数 * * @author 芋道源码 * @see 阿里云 - 删除拓扑关系 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java index 7a61af0a5..1da86c950 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java @@ -1,11 +1,12 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import lombok.Data; /** * IoT 设备拓扑关系获取 Request DTO *

- * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展) + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 请求的 params 参数(目前为空,预留扩展) * * @author 芋道源码 * @see 阿里云 - 获取拓扑关系 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java index 69c9b1555..0aef9c868 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import lombok.Data; @@ -8,7 +9,7 @@ import java.util.List; /** * IoT 设备拓扑关系获取 Response DTO *

- * 用于 thing.topo.get 响应 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 响应 * * @author 芋道源码 * @see 阿里云 - 获取拓扑关系 diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java index 609d0a60a..1aa9cfcab 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java @@ -25,6 +25,14 @@ public class IotDeviceAuthUtils { return String.format("%s.%s", productKey, deviceName); } + public static String buildClientIdFromUsername(String username) { + IotDeviceIdentity identity = parseUsername(username); + if (identity == null) { + return null; + } + return buildClientId(identity.getProductKey(), identity.getDeviceName()); + } + public static String buildUsername(String productKey, String deviceName) { return String.format("%s&%s", deviceName, productKey); } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java new file mode 100644 index 000000000..12d1229d1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.core.util; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; + +/** + * IoT 产品【动态注册】认证工具类 + *

+ * 用于一型一密场景,使用 productSecret 生成签名 + * + * @author 芋道源码 + */ +public class IotProductAuthUtils { + + /** + * 生成设备动态注册签名 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param productSecret 产品密钥 + * @return 签名 + */ + public static String buildSign(String productKey, String deviceName, String productSecret) { + String content = buildContent(productKey, deviceName); + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret)) + .digestHex(content); + } + + /** + * 验证设备动态注册签名 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param productSecret 产品密钥 + * @param sign 待验证的签名 + * @return 是否验证通过 + */ + public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) { + String expectedSign = buildSign(productKey, deviceName, productSecret); + return expectedSign.equals(sign); + } + + /** + * 构建签名内容 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @return 签名内容 + */ + private static String buildContent(String productKey, String deviceName) { + return "deviceName" + deviceName + "productKey" + productKey; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml index 9bf984ef4..ea2199580 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/pom.xml +++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml @@ -33,7 +33,7 @@ org.apache.rocketmq rocketmq-spring-boot-starter - + true @@ -48,6 +48,12 @@ vertx-mqtt + + + com.ghgande + j2mod + + org.eclipse.californium diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java deleted file mode 100644 index 2fcea2e46..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; - -/** - * {@link IotDeviceMessage} 的编解码器 - * - * @author 芋道源码 - */ -public interface IotDeviceMessageCodec { - - /** - * 编码消息 - * - * @param message 消息 - * @return 编码后的消息内容 - */ - byte[] encode(IotDeviceMessage message); - - /** - * 解码消息 - * - * @param bytes 消息内容 - * @return 解码后的消息内容 - */ - IotDeviceMessage decode(byte[] bytes); - - /** - * @return 数据格式(编码器类型) - */ - String type(); - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java deleted file mode 100644 index 5a4e47fe1..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java +++ /dev/null @@ -1,89 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.alink; - -import cn.hutool.core.lang.Assert; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * 阿里云 Alink {@link IotDeviceMessage} 的编解码器 - * - * @author 芋道源码 - */ -@Component -public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "Alink"; - - @Data - @NoArgsConstructor - @AllArgsConstructor - private static class AlinkMessage { - - public static final String VERSION_1 = "1.0"; - - /** - * 消息 ID,且每个消息 ID 在当前设备具有唯一性 - */ - private String id; - - /** - * 版本号 - */ - private String version; - - /** - * 请求方法 - */ - private String method; - - /** - * 请求参数 - */ - private Object params; - - /** - * 响应结果 - */ - private Object data; - /** - * 响应错误码 - */ - private Integer code; - /** - * 响应提示 - * - * 特殊:这里阿里云是 message,为了保持和项目的 {@link CommonResult#getMsg()} 一致。 - */ - private String msg; - - } - - @Override - public String type() { - return TYPE; - } - - @Override - public byte[] encode(IotDeviceMessage message) { - AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1, - message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg()); - return JsonUtils.toJsonByte(alinkMessage); - } - - @Override - @SuppressWarnings("DataFlowIssue") - public IotDeviceMessage decode(byte[] bytes) { - AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class); - Assert.notNull(alinkMessage, "消息不能为空"); - Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0"); - return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(), - alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java deleted file mode 100644 index e1dae7707..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 提供设备接入的各种数据(请求、响应)的编解码 - */ -package cn.iocoder.yudao.module.iot.gateway.codec; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java deleted file mode 100644 index 5bd676ad1..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * TODO @芋艿:实现一个 alink 的 xml 版本 - */ -package cn.iocoder.yudao.module.iot.gateway.codec.simple; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java deleted file mode 100644 index 7d62ce2e0..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器 - * - * 采用纯 JSON 格式传输,格式如下: - * { - * "id": "消息 ID", - * "method": "消息方法", - * "params": {...}, // 请求参数 - * "data": {...}, // 响应结果 - * "code": 200, // 响应错误码 - * "msg": "success", // 响应提示 - * "timestamp": 时间戳 - * } - * - * @author 芋道源码 - */ -@Component -public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "TCP_JSON"; - - @Data - @NoArgsConstructor - @AllArgsConstructor - private static class TcpJsonMessage { - - /** - * 消息 ID,且每个消息 ID 在当前设备具有唯一性 - */ - private String id; - - /** - * 请求方法 - */ - private String method; - - /** - * 请求参数 - */ - private Object params; - - /** - * 响应结果 - */ - private Object data; - - /** - * 响应错误码 - */ - private Integer code; - - /** - * 响应提示 - */ - private String msg; - - /** - * 时间戳 - */ - private Long timestamp; - - } - - @Override - public String type() { - return TYPE; - } - - @Override - public byte[] encode(IotDeviceMessage message) { - TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( - message.getRequestId(), - message.getMethod(), - message.getParams(), - message.getData(), - message.getCode(), - message.getMsg(), - System.currentTimeMillis()); - return JsonUtils.toJsonByte(tcpJsonMessage); - } - - @Override - @SuppressWarnings("DataFlowIssue") - public IotDeviceMessage decode(byte[] bytes) { - String jsonStr = StrUtil.utf8Str(bytes).trim(); - TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class); - Assert.notNull(tcpJsonMessage, "消息不能为空"); - Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); - return IotDeviceMessage.of( - tcpJsonMessage.getId(), - tcpJsonMessage.getMethod(), - tcpJsonMessage.getParams(), - tcpJsonMessage.getData(), - tcpJsonMessage.getCode(), - tcpJsonMessage.getMsg()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index a4e93a84f..5c2fd860e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -1,254 +1,28 @@ package cn.iocoder.yudao.module.iot.gateway.config; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * IoT 网关配置类 + * + * @author 芋道源码 + */ @Configuration @EnableConfigurationProperties(IotGatewayProperties.class) -@Slf4j public class IotGatewayConfiguration { - /** - * IoT 网关 HTTP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true") - @Slf4j - public static class HttpProtocolConfiguration { - - @Bean(name = "httpVertx", destroyMethod = "close") - public Vertx httpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties, - @Qualifier("httpVertx") Vertx httpVertx) { - return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx); - } - - @Bean - public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol, - IotMessageBus messageBus) { - return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus); - } + @Bean + public IotMessageSerializerManager iotMessageSerializerManager() { + return new IotMessageSerializerManager(); } - /** - * IoT 网关 EMQX 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true") - @Slf4j - public static class EmqxProtocolConfiguration { - - @Bean(name = "emqxVertx", destroyMethod = "close") - public Vertx emqxVertx() { - return Vertx.vertx(); - } - - @Bean - public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties, - @Qualifier("emqxVertx") Vertx emqxVertx) { - return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); - } - - @Bean - public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties, - @Qualifier("emqxVertx") Vertx emqxVertx) { - return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); - } - - @Bean - public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, - IotMessageBus messageBus) { - return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus); - } - } - - /** - * IoT 网关 TCP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true") - @Slf4j - public static class TcpProtocolConfiguration { - - @Bean(name = "tcpVertx", destroyMethod = "close") - public Vertx tcpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager, - @Qualifier("tcpVertx") Vertx tcpVertx) { - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), - deviceService, messageService, connectionManager, tcpVertx); - } - - @Bean - public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, - IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager, - IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus); - } - - } - - /** - * IoT 网关 MQTT 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true") - @Slf4j - public static class MqttProtocolConfiguration { - - @Bean(name = "mqttVertx", destroyMethod = "close") - public Vertx mqttVertx() { - return Vertx.vertx(); - } - - @Bean - public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceMessageService messageService, - IotMqttConnectionManager connectionManager, - @Qualifier("mqttVertx") Vertx mqttVertx) { - return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService, - connectionManager, mqttVertx); - } - - @Bean - public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService, - IotMqttConnectionManager connectionManager) { - return new IotMqttDownstreamHandler(messageService, connectionManager); - } - - @Bean - public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol, - IotMqttDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { - return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus); - } - - } - - /** - * IoT 网关 UDP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true") - @Slf4j - public static class UdpProtocolConfiguration { - - @Bean(name = "udpVertx", destroyMethod = "close") - public Vertx udpVertx() { - return Vertx.vertx(); - } - - @Bean - public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - @Qualifier("udpVertx") Vertx udpVertx) { - return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(), - deviceService, messageService, sessionManager, udpVertx); - } - - @Bean - public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - IotMessageBus messageBus) { - return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus); - } - - } - - /** - * IoT 网关 CoAP 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true") - @Slf4j - public static class CoapProtocolConfiguration { - - @Bean - public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) { - return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap()); - } - - @Bean - public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol, - IotMessageBus messageBus) { - return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus); - } - - } - - /** - * IoT 网关 WebSocket 协议配置类 - */ - @Configuration - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true") - @Slf4j - public static class WebSocketProtocolConfiguration { - - @Bean(name = "websocketVertx", destroyMethod = "close") - public Vertx websocketVertx() { - return Vertx.vertx(); - } - - @Bean - public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, - @Qualifier("websocketVertx") Vertx websocketVertx) { - return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(), - deviceService, messageService, connectionManager, websocketVertx); - } - - @Bean - public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, - IotMessageBus messageBus) { - return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus); - } - + @Bean + public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties) { + return new IotProtocolManager(gatewayProperties); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 9a86ee600..63894dc9d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -1,5 +1,16 @@ package cn.iocoder.yudao.module.iot.gateway.config; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -24,9 +35,9 @@ public class IotGatewayProperties { private TokenProperties token; /** - * 协议配置 + * 协议实例列表 */ - private ProtocolProperties protocol; + private List protocols; @Data public static class RpcProperties { @@ -65,582 +76,158 @@ public class IotGatewayProperties { } + /** + * 协议实例配置 + */ @Data public static class ProtocolProperties { /** - * HTTP 组件配置 + * 协议实例 ID,如 "http-alink"、"tcp-binary" */ - private HttpProperties http; - + @NotEmpty(message = "协议实例 ID 不能为空") + private String id; /** - * EMQX 组件配置 + * 是否启用 */ - private EmqxProperties emqx; - + @NotNull(message = "是否启用不能为空") + private Boolean enabled = true; /** - * TCP 组件配置 + * 协议类型 + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum */ - private TcpProperties tcp; - - /** - * MQTT 组件配置 - */ - private MqttProperties mqtt; - - /** - * MQTT WebSocket 组件配置 - */ - private MqttWsProperties mqttWs; - - /** - * UDP 组件配置 - */ - private UdpProperties udp; - - /** - * CoAP 组件配置 - */ - private CoapProperties coap; - - /** - * WebSocket 组件配置 - */ - private WebSocketProperties websocket; - - } - - @Data - public static class HttpProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; + @NotEmpty(message = "协议类型不能为空") + private String protocol; /** * 服务端口 - */ - private Integer serverPort; - - /** - * 是否开启 SSL - */ - @NotNull(message = "是否开启 SSL 不能为空") - private Boolean sslEnabled = false; - - /** - * SSL 证书路径 - */ - private String sslKeyPath; - /** - * SSL 证书路径 - */ - private String sslCertPath; - - } - - @Data - public static class EmqxProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * HTTP 服务端口(默认:8090) - */ - private Integer httpPort = 8090; - - /** - * MQTT 服务器地址 - */ - @NotEmpty(message = "MQTT 服务器地址不能为空") - private String mqttHost; - - /** - * MQTT 服务器端口(默认:1883) - */ - @NotNull(message = "MQTT 服务器端口不能为空") - private Integer mqttPort = 1883; - - /** - * MQTT 用户名 - */ - @NotEmpty(message = "MQTT 用户名不能为空") - private String mqttUsername; - - /** - * MQTT 密码 - */ - @NotEmpty(message = "MQTT 密码不能为空") - private String mqttPassword; - - /** - * MQTT 客户端的 SSL 开关 - */ - @NotNull(message = "MQTT 是否开启 SSL 不能为空") - private Boolean mqttSsl = false; - - /** - * MQTT 客户端 ID(如果为空,系统将自动生成) - */ - @NotEmpty(message = "MQTT 客户端 ID 不能为空") - private String mqttClientId; - - /** - * MQTT 订阅的主题 - */ - @NotEmpty(message = "MQTT 主题不能为空") - private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; - - /** - * 默认 QoS 级别 *

- * 0 - 最多一次 - * 1 - 至少一次 - * 2 - 刚好一次 + * 不同协议含义不同: + * 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP:对应网关自身监听的服务端口 + * 2. EMQX:对应网关提供给 EMQX 回调的 HTTP Hook 端口(/mqtt/auth、/mqtt/acl、/mqtt/event) */ - private Integer mqttQos = 1; + @NotNull(message = "服务端口不能为空") + private Integer port; + /** + * 序列化类型(可选) + * + * @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum + * + * 为什么是可选的呢? + * 1. {@link IotProtocolTypeEnum#HTTP}、{@link IotProtocolTypeEnum#COAP} 协议,目前强制是 JSON 格式 + * 2. {@link IotProtocolTypeEnum#EMQX} 协议,目前支持根据产品(设备)配置的序列化类型来解析 + */ + private String serialize; + + // ========== SSL 配置 ========== /** - * 连接超时时间(秒) + * SSL 配置(可选,配置文件中不配置则为 null) */ - private Integer connectTimeoutSeconds = 10; + @Valid + private SslConfig ssl; + + // ========== 各协议配置 ========== /** - * 重连延迟时间(毫秒) + * HTTP 协议配置 */ - private Long reconnectDelayMs = 5000L; + @Valid + private IotHttpConfig http; + /** + * WebSocket 协议配置 + */ + @Valid + private IotWebSocketConfig websocket; /** - * 是否启用 Clean Session (清理会话) - * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 - * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 + * TCP 协议配置 */ - private Boolean cleanSession = true; + @Valid + private IotTcpConfig tcp; + /** + * UDP 协议配置 + */ + @Valid + private IotUdpConfig udp; /** - * 心跳间隔(秒) - * 用于保持连接活性,及时发现网络中断。 + * CoAP 协议配置 */ - private Integer keepAliveIntervalSeconds = 60; + @Valid + private IotCoapConfig coap; /** - * 最大未确认消息队列大小 - * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 + * MQTT 协议配置 */ - private Integer maxInflightQueue = 10000; + @Valid + private IotMqttConfig mqtt; + /** + * EMQX 协议配置 + */ + @Valid + private IotEmqxConfig emqx; /** - * 是否信任所有 SSL 证书 - * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! - * 在生产环境中,应设置为 false,并配置正确的信任库。 + * Modbus TCP Client 协议配置 */ - private Boolean trustAll = false; + @Valid + private IotModbusTcpClientConfig modbusTcpClient; /** - * 遗嘱消息配置 (用于网关异常下线时通知其他系统) + * Modbus TCP Server 协议配置 */ - private final Will will = new Will(); - - /** - * 高级 SSL/TLS 配置 (用于生产环境) - */ - private final Ssl sslOptions = new Ssl(); - - /** - * 遗嘱消息 (Last Will and Testament) - */ - @Data - public static class Will { - - /** - * 是否启用遗嘱消息 - */ - private boolean enabled = false; - /** - * 遗嘱消息主题 - */ - private String topic; - /** - * 遗嘱消息内容 - */ - private String payload; - /** - * 遗嘱消息 QoS 等级 - */ - private Integer qos = 1; - /** - * 遗嘱消息是否作为保留消息发布 - */ - private boolean retain = true; - - } - - /** - * 高级 SSL/TLS 配置 - */ - @Data - public static class Ssl { - - /** - * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks - * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 - */ - private String keyStorePath; - /** - * 密钥库密码 - */ - private String keyStorePassword; - /** - * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks - * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 - */ - private String trustStorePath; - /** - * 信任库密码 - */ - private String trustStorePassword; - - } + @Valid + private IotModbusTcpServerConfig modbusTcpServer; } + /** + * SSL 配置 + */ @Data - public static class TcpProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务器端口 - */ - private Integer port = 8091; - - /** - * 心跳超时时间(毫秒) - */ - private Long keepAliveTimeoutMs = 30000L; - - /** - * 最大连接数 - */ - private Integer maxConnections = 1000; - - /** - * 是否启用SSL - */ - private Boolean sslEnabled = false; - - /** - * SSL证书路径 - */ - private String sslCertPath; - - /** - * SSL私钥路径 - */ - private String sslKeyPath; - - } - - @Data - public static class MqttProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务器端口 - */ - private Integer port = 1883; - - /** - * 最大消息大小(字节) - */ - private Integer maxMessageSize = 8192; - - /** - * 连接超时时间(秒) - */ - private Integer connectTimeoutSeconds = 60; - /** - * 保持连接超时时间(秒) - */ - private Integer keepAliveTimeoutSeconds = 300; + public static class SslConfig { /** * 是否启用 SSL */ - private Boolean sslEnabled = false; - /** - * SSL 配置 - */ - private SslOptions sslOptions = new SslOptions(); - - /** - * SSL 配置选项 - */ - @Data - public static class SslOptions { - - /** - * 密钥证书选项 - */ - private io.vertx.core.net.KeyCertOptions keyCertOptions; - /** - * 信任选项 - */ - private io.vertx.core.net.TrustOptions trustOptions; - /** - * SSL 证书路径 - */ - private String certPath; - /** - * SSL 私钥路径 - */ - private String keyPath; - /** - * 信任存储路径 - */ - private String trustStorePath; - /** - * 信任存储密码 - */ - private String trustStorePassword; - - } - - } - - @Data - public static class MqttWsProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * WebSocket 服务器端口(默认:8083) - */ - private Integer port = 8083; - - /** - * WebSocket 路径(默认:/mqtt) - */ - @NotEmpty(message = "WebSocket 路径不能为空") - private String path = "/mqtt"; - - /** - * 最大消息大小(字节) - */ - private Integer maxMessageSize = 8192; - - /** - * 连接超时时间(秒) - */ - private Integer connectTimeoutSeconds = 60; - - /** - * 保持连接超时时间(秒) - */ - private Integer keepAliveTimeoutSeconds = 300; - - /** - * 是否启用 SSL(wss://) - */ - private Boolean sslEnabled = false; - - /** - * SSL 配置 - */ - private SslOptions sslOptions = new SslOptions(); - - /** - * WebSocket 子协议(通常为 "mqtt" 或 "mqttv3.1") - */ - @NotEmpty(message = "WebSocket 子协议不能为空") - private String subProtocol = "mqtt"; - - /** - * 最大帧大小(字节) - */ - private Integer maxFrameSize = 65536; - - /** - * SSL 配置选项 - */ - @Data - public static class SslOptions { - - /** - * 密钥证书选项 - */ - private io.vertx.core.net.KeyCertOptions keyCertOptions; - - /** - * 信任选项 - */ - private io.vertx.core.net.TrustOptions trustOptions; - - /** - * SSL 证书路径 - */ - private String certPath; - - /** - * SSL 私钥路径 - */ - private String keyPath; - - /** - * 信任存储路径 - */ - private String trustStorePath; - - /** - * 信任存储密码 - */ - private String trustStorePassword; - - } - - } - - @Data - public static class UdpProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务端口(默认 8093) - */ - private Integer port = 8093; - - /** - * 接收缓冲区大小(默认 64KB) - */ - private Integer receiveBufferSize = 65536; - - /** - * 发送缓冲区大小(默认 64KB) - */ - private Integer sendBufferSize = 65536; - - /** - * 会话超时时间(毫秒,默认 60 秒) - *

- * 用于清理不活跃的设备地址映射 - */ - private Long sessionTimeoutMs = 60000L; - - /** - * 会话清理间隔(毫秒,默认 30 秒) - */ - private Long sessionCleanIntervalMs = 30000L; - - } - - @Data - public static class CoapProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务端口(CoAP 默认端口 5683) - */ - @NotNull(message = "服务端口不能为空") - private Integer port = 5683; - - /** - * 最大消息大小(字节) - */ - @NotNull(message = "最大消息大小不能为空") - private Integer maxMessageSize = 1024; - - /** - * ACK 超时时间(毫秒) - */ - @NotNull(message = "ACK 超时时间不能为空") - private Integer ackTimeout = 2000; - - /** - * 最大重传次数 - */ - @NotNull(message = "最大重传次数不能为空") - private Integer maxRetransmit = 4; - - } - - @Data - public static class WebSocketProperties { - - /** - * 是否开启 - */ - @NotNull(message = "是否开启不能为空") - private Boolean enabled; - - /** - * 服务器端口(默认:8094) - */ - private Integer port = 8094; - - /** - * WebSocket 路径(默认:/ws) - */ - @NotEmpty(message = "WebSocket 路径不能为空") - private String path = "/ws"; - - /** - * 最大消息大小(字节,默认 64KB) - */ - private Integer maxMessageSize = 65536; - - /** - * 最大帧大小(字节,默认 64KB) - */ - private Integer maxFrameSize = 65536; - - /** - * 空闲超时时间(秒,默认 60) - */ - private Integer idleTimeoutSeconds = 60; - - /** - * 是否启用 SSL(wss://) - */ - private Boolean sslEnabled = false; + @NotNull(message = "是否启用 SSL 不能为空") + private Boolean ssl = false; /** * SSL 证书路径 */ + @NotEmpty(message = "SSL 证书路径不能为空") private String sslCertPath; /** * SSL 私钥路径 */ + @NotEmpty(message = "SSL 私钥路径不能为空") private String sslKeyPath; + /** + * 密钥库(KeyStore)路径 + *

+ * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证) + */ + private String keyStorePath; + /** + * 密钥库密码 + */ + private String keyStorePassword; + + /** + * 信任库(TrustStore)路径 + *

+ * 包含服务端信任的 CA 证书,用于验证服务端的身份 + */ + private String trustStorePath; + /** + * 信任库密码 + */ + private String trustStorePassword; + } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java similarity index 57% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java index 61bf12376..efd61e13a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java @@ -1,49 +1,53 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; +package cn.iocoder.yudao.module.iot.gateway.protocol; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler; -import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; /** - * IoT 网关 EMQX 订阅者:接收下行给设备的消息 + * IoT 协议下行消息订阅者抽象类 + * + * 负责接收来自消息总线的下行消息,并委托给子类进行业务处理 * * @author 芋道源码 */ +@AllArgsConstructor @Slf4j -public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber { +public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber { - private final IotEmqxDownstreamHandler downstreamHandler; + private final IotProtocol protocol; private final IotMessageBus messageBus; - private final IotEmqxUpstreamProtocol protocol; - - public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { - this.protocol = protocol; - this.messageBus = messageBus; - this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); - } - - @PostConstruct - public void init() { - messageBus.register(this); - } - @Override public String getTopic() { return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); } + /** + * 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + */ @Override public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group return getTopic(); } + @Override + public void start() { + messageBus.register(this); + log.info("[start][{} 下行消息订阅成功,Topic:{}]", protocol.getType().name(), getTopic()); + } + + @Override + public void stop() { + messageBus.unregister(this); + log.info("[stop][{} 下行消息订阅已停止,Topic:{}]", protocol.getType().name(), getTopic()); + } + @Override public void onMessage(IotDeviceMessage message) { log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", @@ -51,18 +55,25 @@ public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber protocols = new ArrayList<>(); + + @Getter + private volatile boolean running = false; + + public IotProtocolManager(IotGatewayProperties gatewayProperties) { + this.gatewayProperties = gatewayProperties; + } + + @Override + public void start() { + if (running) { + return; + } + List protocolConfigs = gatewayProperties.getProtocols(); + if (CollUtil.isEmpty(protocolConfigs)) { + log.info("[start][没有配置协议实例,跳过启动]"); + return; + } + + for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) { + if (BooleanUtil.isFalse(config.getEnabled())) { + log.info("[start][协议实例 {} 未启用,跳过]", config.getId()); + continue; + } + IotProtocol protocol = createProtocol(config); + if (protocol == null) { + continue; + } + protocol.start(); + protocols.add(protocol); + } + running = true; + log.info("[start][协议管理器启动完成,共启动 {} 个协议实例]", protocols.size()); + } + + @Override + public void stop() { + if (!running) { + return; + } + for (IotProtocol protocol : protocols) { + try { + protocol.stop(); + } catch (Exception e) { + log.error("[stop][协议实例 {} 停止失败]", protocol.getId(), e); + } + } + protocols.clear(); + running = false; + log.info("[stop][协议管理器已停止]"); + } + + /** + * 创建协议实例 + * + * @param config 协议实例配置 + * @return 协议实例 + */ + @SuppressWarnings({"EnhancedSwitchMigration"}) + private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) { + IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol()); + if (protocolType == null) { + log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol()); + return null; + } + switch (protocolType) { + case HTTP: + return createHttpProtocol(config); + case TCP: + return createTcpProtocol(config); + case UDP: + return createUdpProtocol(config); + case COAP: + return createCoapProtocol(config); + case WEBSOCKET: + return createWebSocketProtocol(config); + case MQTT: + return createMqttProtocol(config); + case EMQX: + return createEmqxProtocol(config); + case MODBUS_TCP_CLIENT: + return createModbusTcpClientProtocol(config); + case MODBUS_TCP_SERVER: + return createModbusTcpServerProtocol(config); + default: + throw new IllegalArgumentException(String.format( + "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType)); + } + } + + /** + * 创建 HTTP 协议实例 + * + * @param config 协议实例配置 + * @return HTTP 协议实例 + */ + private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotHttpProtocol(config); + } + + /** + * 创建 TCP 协议实例 + * + * @param config 协议实例配置 + * @return TCP 协议实例 + */ + private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotTcpProtocol(config); + } + + /** + * 创建 UDP 协议实例 + * + * @param config 协议实例配置 + * @return UDP 协议实例 + */ + private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotUdpProtocol(config); + } + + /** + * 创建 CoAP 协议实例 + * + * @param config 协议实例配置 + * @return CoAP 协议实例 + */ + private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotCoapProtocol(config); + } + + /** + * 创建 WebSocket 协议实例 + * + * @param config 协议实例配置 + * @return WebSocket 协议实例 + */ + private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotWebSocketProtocol(config); + } + + /** + * 创建 MQTT 协议实例 + * + * @param config 协议实例配置 + * @return MQTT 协议实例 + */ + private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotMqttProtocol(config); + } + + /** + * 创建 EMQX 协议实例 + * + * @param config 协议实例配置 + * @return EMQX 协议实例 + */ + private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotEmqxProtocol(config); + } + + /** + * 创建 Modbus TCP Client 协议实例 + * + * @param config 协议实例配置 + * @return Modbus TCP Client 协议实例 + */ + private IotModbusTcpClientProtocol createModbusTcpClientProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotModbusTcpClientProtocol(config); + } + + /** + * 创建 Modbus TCP Server 协议实例 + * + * @param config 协议实例配置 + * @return Modbus TCP Server 协议实例 + */ + private IotModbusTcpServerProtocol createModbusTcpServerProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotModbusTcpServerProtocol(config); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java new file mode 100644 index 000000000..45fe3007e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapConfig.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT CoAP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotCoapConfig { + + /** + * 最大消息大小(字节) + */ + @NotNull(message = "最大消息大小不能为空") + @Min(value = 64, message = "最大消息大小必须大于 64 字节") + private Integer maxMessageSize = 1024; + + /** + * ACK 超时时间(毫秒) + */ + @NotNull(message = "ACK 超时时间不能为空") + @Min(value = 100, message = "ACK 超时时间必须大于 100 毫秒") + private Integer ackTimeoutMs = 2000; + + /** + * 最大重传次数 + */ + @NotNull(message = "最大重传次数不能为空") + @Min(value = 0, message = "最大重传次数必须大于等于 0") + private Integer maxRetransmit = 4; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java deleted file mode 100644 index d01cdc416..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 CoAP 订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotCoapDownstreamSubscriber implements IotMessageSubscriber { - - private final IotCoapUpstreamProtocol protocol; - - private final IotMessageBus messageBus; - - @PostConstruct - public void init() { - messageBus.register(this); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更) - log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java new file mode 100644 index 000000000..d797ef8bc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java @@ -0,0 +1,168 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.*; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.config.CoapConfig; +import org.eclipse.californium.elements.config.Configuration; + +import java.util.concurrent.TimeUnit; + +/** + * IoT CoAP 协议实现 + *

+ * 基于 Eclipse Californium 实现,支持: + * 1. 认证:POST /auth + * 2. 设备动态注册:POST /auth/register/device + * 3. 子设备动态注册:POST /auth/register/sub-device/{productKey}/{deviceName} + * 4. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post + * 5. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * CoAP 服务器 + */ + private CoapServer coapServer; + + /** + * 下行消息订阅者 + */ + private IotCoapDownstreamSubscriber downstreamSubscriber; + + public IotCoapProtocol(ProtocolProperties properties) { + IotCoapConfig coapConfig = properties.getCoap(); + Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.COAP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT CoAP 协议 {} 已经在运行中]", getId()); + return; + } + + try { + // 1.1 创建 CoAP 配置 + IotCoapConfig coapConfig = properties.getCoap(); + Configuration config = Configuration.createStandardWithoutFile(); + config.set(CoapConfig.COAP_PORT, properties.getPort()); + config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize()); + config.set(CoapConfig.ACK_TIMEOUT, coapConfig.getAckTimeoutMs(), TimeUnit.MILLISECONDS); + config.set(CoapConfig.MAX_RETRANSMIT, coapConfig.getMaxRetransmit()); + // 1.2 创建 CoAP 服务器 + coapServer = new CoapServer(config); + + // 2.1 添加 /auth 认证资源 + IotCoapAuthHandler authHandler = new IotCoapAuthHandler(serverId); + IotCoapAuthResource authResource = new IotCoapAuthResource(authHandler); + coapServer.add(authResource); + // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) + IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); + IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); + // 2.3 添加 /auth/register/sub-device/{productKey}/{deviceName} 子设备动态注册资源 + IotCoapRegisterSubHandler registerSubHandler = new IotCoapRegisterSubHandler(); + IotCoapRegisterSubResource registerSubResource = new IotCoapRegisterSubResource(registerSubHandler); + authResource.add(new CoapResource("register") {{ + add(registerResource); + add(registerSubResource); + }}); + // 2.4 添加 /topic 根资源(用于上行消息) + IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(serverId); + IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(serverId, upstreamHandler); + coapServer.add(topicResource); + + // 3. 启动服务器 + coapServer.start(); + running = true; + log.info("[start][IoT CoAP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 4. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus); + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e); + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2. 关闭 CoAP 服务器 + if (coapServer != null) { + try { + coapServer.stop(); + coapServer.destroy(); + coapServer = null; + log.info("[stop][IoT CoAP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT CoAP 协议 {} 服务器停止失败]", getId(), e); + } + } + running = false; + log.info("[stop][IoT CoAP 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java deleted file mode 100644 index e10bd9889..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java +++ /dev/null @@ -1,90 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap; - -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.*; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapResource; -import org.eclipse.californium.core.CoapServer; -import org.eclipse.californium.core.config.CoapConfig; -import org.eclipse.californium.elements.config.Configuration; - -import java.util.concurrent.TimeUnit; - -/** - * IoT 网关 CoAP 协议:接收设备上行消息 - * - * 基于 Eclipse Californium 实现,支持: - * 1. 认证:POST /auth - * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapUpstreamProtocol { - - private final IotGatewayProperties.CoapProperties coapProperties; - - private CoapServer coapServer; - - @Getter - private final String serverId; - - public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) { - this.coapProperties = coapProperties; - this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort()); - } - - @PostConstruct - public void start() { - try { - // 1.1 创建网络配置(Californium 3.x API) - Configuration config = Configuration.createStandardWithoutFile(); - config.set(CoapConfig.COAP_PORT, coapProperties.getPort()); - config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize()); - config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS); - config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit()); - // 1.2 创建 CoAP 服务器 - coapServer = new CoapServer(config); - - // 2.1 添加 /auth 认证资源 - IotCoapAuthHandler authHandler = new IotCoapAuthHandler(); - IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler); - coapServer.add(authResource); - // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密) - IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler(); - IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler); - authResource.add(new CoapResource("register") {{ - add(registerResource); - }}); - // 2.3 添加 /topic 根资源(用于上行消息) - IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(); - IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler); - coapServer.add(topicResource); - - // 3. 启动服务器 - coapServer.start(); - log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 CoAP 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (coapServer != null) { - try { - coapServer.stop(); - log.info("[stop][IoT 网关 CoAP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 CoAP 协议停止失败]", e); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java new file mode 100644 index 000000000..3309d2cd4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 CoAP 订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { + + public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) { + super(protocol, messageBus); + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更) + log.warn("[handleMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java new file mode 100644 index 000000000..994fb147d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAbstractHandler.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Option; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * IoT 网关 CoAP 协议的处理器抽象基类:提供通用的前置处理(认证)、请求解析、响应处理、全局的异常捕获等 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class IotCoapAbstractHandler { + + /** + * 自定义 CoAP Option 编号,用于携带 Token + *

+ * CoAP Option 范围 2048-65535 属于实验/自定义范围 + */ + public static final int OPTION_TOKEN = 2088; + + private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + + /** + * 处理 CoAP 请求(模板方法) + * + * @param exchange CoAP 交换对象 + */ + public final void handle(CoapExchange exchange) { + try { + // 1. 前置处理 + beforeHandle(exchange); + + // 2. 执行业务逻辑 + CommonResult result = handle0(exchange); + writeResponse(exchange, result); + } catch (ServiceException e) { + // 业务异常,返回对应的错误码和消息 + writeResponse(exchange, CommonResult.error(e.getCode(), e.getMessage())); + } catch (IllegalArgumentException e) { + // 参数校验异常(hutool Assert 抛出),返回 BAD_REQUEST + writeResponse(exchange, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage())); + } catch (Exception e) { + // 其他未知异常,返回 INTERNAL_SERVER_ERROR + log.error("[handle][CoAP 请求处理异常]", e); + writeResponse(exchange, CommonResult.error(INTERNAL_SERVER_ERROR)); + } + } + + /** + * 处理 CoAP 请求(子类实现) + * + * @param exchange CoAP 交换对象 + * @return 处理结果 + */ + protected abstract CommonResult handle0(CoapExchange exchange); + + /** + * 前置处理:认证等 + * + * @param exchange CoAP 交换对象 + */ + private void beforeHandle(CoapExchange exchange) { + // 1.1 如果不需要认证,则不走前置处理 + if (!requiresAuthentication()) { + return; + } + // 1.2 从自定义 Option 获取 token + String token = getTokenFromOption(exchange); + if (StrUtil.isEmpty(token)) { + throw exception(UNAUTHORIZED); + } + // 1.3 校验 token + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); + if (deviceInfo == null) { + throw exception(UNAUTHORIZED); + } + + // 2.1 解析 productKey 和 deviceName + List uriPath = exchange.getRequestOptions().getUriPath(); + String productKey = getProductKey(uriPath); + String deviceName = getDeviceName(uriPath); + if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceName)) { + throw exception(BAD_REQUEST); + } + // 2.2 校验设备信息是否匹配 + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { + throw exception(FORBIDDEN); + } + } + + // ========== Token 相关方法 ========== + + /** + * 是否需要认证(子类可覆盖) + *

+ * 默认不需要认证 + * + * @return 是否需要认证 + */ + protected boolean requiresAuthentication() { + return false; + } + + /** + * 从 URI 路径中获取 productKey(子类实现) + *

+ * 默认抛出异常,需要认证的子类必须实现此方法 + * + * @param uriPath URI 路径 + * @return productKey + */ + protected String getProductKey(List uriPath) { + throw new UnsupportedOperationException("子类需要实现 getProductKey 方法"); + } + + /** + * 从 URI 路径中获取 deviceName(子类实现) + *

+ * 默认抛出异常,需要认证的子类必须实现此方法 + * + * @param uriPath URI 路径 + * @return deviceName + */ + protected String getDeviceName(List uriPath) { + throw new UnsupportedOperationException("子类需要实现 getDeviceName 方法"); + } + + /** + * 从自定义 CoAP Option 中获取 Token + * + * @param exchange CoAP 交换对象 + * @return Token 值,如果不存在则返回 null + */ + protected String getTokenFromOption(CoapExchange exchange) { + Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(), + o -> o.getNumber() == OPTION_TOKEN); + return option != null ? new String(option.getValue()) : null; + } + + // ========== 序列化相关方法 ========== + + /** + * 解析请求体为指定类型 + * + * @param exchange CoAP 交换对象 + * @param clazz 目标类型 + * @param 目标类型泛型 + * @return 解析后的对象,解析失败返回 null + */ + protected T deserializeRequest(CoapExchange exchange, Class clazz) { + byte[] payload = exchange.getRequestPayload(); + if (ArrayUtil.isEmpty(payload)) { + return null; + } + return JsonUtils.parseObject(payload, clazz); + } + + private static String serializeResponse(Object data) { + return JsonUtils.toJsonString(data); + } + + protected void writeResponse(CoapExchange exchange, CommonResult data) { + String json = serializeResponse(data); + exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java new file mode 100644 index 000000000..0b1914e09 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthHandler.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * IoT 网关 CoAP 协议的【认证】处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapAuthHandler extends IotCoapAbstractHandler { + + private final String serverId; + + private final IotDeviceTokenService deviceTokenService; + private final IotDeviceCommonApi deviceApi; + private final IotDeviceMessageService deviceMessageService; + + public IotCoapAuthHandler(String serverId) { + this.serverId = serverId; + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + protected CommonResult handle0(CoapExchange exchange) { + // 1. 解析参数 + IotDeviceAuthReqDTO request = deserializeRequest(exchange, IotDeviceAuthReqDTO.class); + Assert.notNull(request, "请求体不能为空"); + Assert.notBlank(request.getClientId(), "clientId 不能为空"); + Assert.notBlank(request.getUsername(), "username 不能为空"); + Assert.notBlank(request.getPassword(), "password 不能为空"); + + // 2.1 执行认证 + CommonResult result = deviceApi.authDevice(request); + result.checkError(); + if (BooleanUtil.isFalse(result.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 生成 Token + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername()); + Assert.notNull(deviceInfo, "设备信息不能为空"); + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notBlank(token, "生成 token 不能为空"); + + // 3. 执行上线 + IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(message, + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); + + // 4. 构建响应数据 + return CommonResult.success(MapUtil.of("token", token)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthResource.java similarity index 64% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthResource.java index 9d0d90cb3..95b6fefd4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapAuthResource.java @@ -1,6 +1,5 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.server.resources.CoapExchange; @@ -17,13 +16,10 @@ public class IotCoapAuthResource extends CoapResource { public static final String PATH = "auth"; - private final IotCoapUpstreamProtocol protocol; private final IotCoapAuthHandler authHandler; - public IotCoapAuthResource(IotCoapUpstreamProtocol protocol, - IotCoapAuthHandler authHandler) { + public IotCoapAuthResource(IotCoapAuthHandler authHandler) { super(PATH); - this.protocol = protocol; this.authHandler = authHandler; log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH); } @@ -31,7 +27,7 @@ public class IotCoapAuthResource extends CoapResource { @Override public void handlePOST(CoapExchange exchange) { log.debug("[handlePOST][收到 /auth POST 请求]"); - authHandler.handle(exchange, protocol); + authHandler.handle(exchange); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java new file mode 100644 index 000000000..12a70d91b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +/** + * IoT 网关 CoAP 协议的【设备动态注册】处理器 + *

+ * 用于直连设备/网关的一型一密动态注册,不需要认证 + * + * @author 芋道源码 + * @see 阿里云 - 一型一密 + */ +@Slf4j +public class IotCoapRegisterHandler extends IotCoapAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + + public IotCoapRegisterHandler() { + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + protected CommonResult handle0(CoapExchange exchange) { + // 1. 解析参数 + IotDeviceRegisterReqDTO request = deserializeRequest(exchange, IotDeviceRegisterReqDTO.class); + Assert.notNull(request, "请求体不能为空"); + Assert.notBlank(request.getProductKey(), "productKey 不能为空"); + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(request.getSign(), "sign 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(request); + result.checkError(); + + // 3. 构建响应数据 + return CommonResult.success(result.getData()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterResource.java similarity index 92% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterResource.java index 05fd1ec89..f8f6b0cf9 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterResource.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java new file mode 100644 index 000000000..8827cc3db --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubHandler.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * IoT 网关 CoAP 协议的【子设备动态注册】处理器 + *

+ * 用于子设备的动态注册,需要网关认证 + * + * @author 芋道源码 + * @see 阿里云 - 动态注册子设备 + */ +@Slf4j +public class IotCoapRegisterSubHandler extends IotCoapAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + + public IotCoapRegisterSubHandler() { + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + protected CommonResult handle0(CoapExchange exchange) { + // 1.1 解析通用参数(从 URI 路径获取网关设备信息) + List uriPath = exchange.getRequestOptions().getUriPath(); + String productKey = getProductKey(uriPath); + String deviceName = getDeviceName(uriPath); + // 1.2 解析子设备列表 + SubDeviceRegisterRequest request = deserializeRequest(exchange, SubDeviceRegisterRequest.class); + Assert.notNull(request, "请求参数不能为空"); + Assert.notEmpty(request.getParams(), "params 不能为空"); + + // 2. 调用子设备动态注册 + IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() + .setGatewayProductKey(productKey) + .setGatewayDeviceName(deviceName) + .setSubDevices(request.getParams()); + CommonResult> result = deviceApi.registerSubDevices(reqDTO); + result.checkError(); + + // 3. 返回结果 + return success(result.getData()); + } + + @Override + protected boolean requiresAuthentication() { + return true; + } + + @Override + protected String getProductKey(List uriPath) { + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName} + return CollUtil.get(uriPath, 3); + } + + @Override + protected String getDeviceName(List uriPath) { + // 路径格式:/auth/register/sub-device/{productKey}/{deviceName} + return CollUtil.get(uriPath, 4); + } + + @Data + public static class SubDeviceRegisterRequest { + + private List params; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java new file mode 100644 index 000000000..3cc42b606 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterSubResource.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.core.server.resources.Resource; + +/** + * IoT 网关 CoAP 协议的子设备动态注册资源(/auth/register/sub-device/{productKey}/{deviceName}) + *

+ * 用于子设备的动态注册,需要网关认证 + *

+ * 支持动态路径匹配:productKey 和 deviceName 是网关设备的标识 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapRegisterSubResource extends CoapResource { + + public static final String PATH = "sub-device"; + + private final IotCoapRegisterSubHandler registerSubHandler; + + /** + * 创建根资源(/auth/register/sub-device) + */ + public IotCoapRegisterSubResource(IotCoapRegisterSubHandler registerSubHandler) { + this(PATH, registerSubHandler); + log.info("[IotCoapRegisterSubResource][创建 CoAP 子设备动态注册资源: /auth/register/{}]", PATH); + } + + /** + * 创建子资源(动态路径) + */ + private IotCoapRegisterSubResource(String name, IotCoapRegisterSubHandler registerSubHandler) { + super(name); + this.registerSubHandler = registerSubHandler; + } + + @Override + public Resource getChild(String name) { + // 递归创建动态子资源,支持 /sub-device/{productKey}/{deviceName} 路径 + return new IotCoapRegisterSubResource(name, registerSubHandler); + } + + @Override + public void handlePOST(CoapExchange exchange) { + log.debug("[handlePOST][收到子设备动态注册请求]"); + registerSubHandler.handle(exchange); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java new file mode 100644 index 000000000..d9e349ba5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamHandler.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.server.resources.CoapExchange; + +import java.util.List; + +/** + * IoT 网关 CoAP 协议的【上行】处理器 + * + * 处理设备通过 CoAP 协议发送的上行消息,包括: + * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post + * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post + * + * Token 通过自定义 CoAP Option 2088 携带 + * + * @author 芋道源码 + */ +@Slf4j +public class IotCoapUpstreamHandler extends IotCoapAbstractHandler { + + private final String serverId; + + private final IotDeviceMessageService deviceMessageService; + + public IotCoapUpstreamHandler(String serverId) { + this.serverId = serverId; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + protected CommonResult handle0(CoapExchange exchange) { + // 1.1 解析通用参数 + List uriPath = exchange.getRequestOptions().getUriPath(); + String productKey = getProductKey(uriPath); + String deviceName = getDeviceName(uriPath); + String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size())); + // 1.2 解析消息 + IotDeviceMessage message = deserializeRequest(exchange, IotDeviceMessage.class); + Assert.notNull(message, "请求参数不能为空"); + Assert.equals(method, message.getMethod(), "method 不匹配"); + + // 2. 发送消息 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + + // 3. 返回结果 + return CommonResult.success(MapUtil.of("messageId", message.getId())); + } + + @Override + protected boolean requiresAuthentication() { + return true; + } + + @Override + protected String getProductKey(List uriPath) { + // 路径格式:/topic/sys/{productKey}/{deviceName}/... + return CollUtil.get(uriPath, 2); + } + + @Override + protected String getDeviceName(List uriPath) { + // 路径格式:/topic/sys/{productKey}/{deviceName}/... + return CollUtil.get(uriPath, 3); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamTopicResource.java similarity index 70% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamTopicResource.java index 1c694483f..65185b575 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapUpstreamTopicResource.java @@ -1,6 +1,5 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.server.resources.CoapExchange; @@ -20,15 +19,15 @@ public class IotCoapUpstreamTopicResource extends CoapResource { public static final String PATH = "topic"; - private final IotCoapUpstreamProtocol protocol; + private final String serverId; private final IotCoapUpstreamHandler upstreamHandler; /** * 创建根资源(/topic) */ - public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol, + public IotCoapUpstreamTopicResource(String serverId, IotCoapUpstreamHandler upstreamHandler) { - this(PATH, protocol, upstreamHandler); + this(PATH, serverId, upstreamHandler); log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH); } @@ -36,32 +35,32 @@ public class IotCoapUpstreamTopicResource extends CoapResource { * 创建子资源(动态路径) */ private IotCoapUpstreamTopicResource(String name, - IotCoapUpstreamProtocol protocol, + String serverId, IotCoapUpstreamHandler upstreamHandler) { super(name); - this.protocol = protocol; + this.serverId = serverId; this.upstreamHandler = upstreamHandler; } @Override public Resource getChild(String name) { // 递归创建动态子资源,支持任意深度路径 - return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler); + return new IotCoapUpstreamTopicResource(name, serverId, upstreamHandler); } @Override public void handleGET(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); + upstreamHandler.handle(exchange); } @Override public void handlePOST(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); + upstreamHandler.handle(exchange); } @Override public void handlePUT(CoapExchange exchange) { - upstreamHandler.handle(exchange, protocol); + upstreamHandler.handle(exchange); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java index 94536a643..3de662a5c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java @@ -2,12 +2,5 @@ * CoAP 协议实现包 *

* 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能 - *

- * URI 路径: - * - 认证:POST /auth - * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - *

- * Token 通过 CoAP Option 2088 携带 */ package cn.iocoder.yudao.module.iot.gateway.protocol.coap; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java deleted file mode 100644 index 43fb77608..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java +++ /dev/null @@ -1,117 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.Map; - -/** - * IoT 网关 CoAP 协议的【认证】处理器 - * - * 参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler} - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapAuthHandler { - - private final IotDeviceTokenService deviceTokenService; - private final IotDeviceCommonApi deviceApi; - private final IotDeviceMessageService deviceMessageService; - - public IotCoapAuthHandler() { - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - } - - /** - * 处理认证请求 - * - * @param exchange CoAP 交换对象 - * @param protocol 协议对象 - */ - @SuppressWarnings("unchecked") - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { - try { - // 1.1 解析请求体 - byte[] payload = exchange.getRequestPayload(); - if (payload == null || payload.length == 0) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - Map body; - try { - body = JsonUtils.parseObject(new String(payload), Map.class); - } catch (Exception e) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); - return; - } - // 1.2 解析参数 - String clientId = MapUtil.getStr(body, "clientId"); - if (StrUtil.isEmpty(clientId)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空"); - return; - } - String username = MapUtil.getStr(body, "username"); - if (StrUtil.isEmpty(username)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空"); - return; - } - String password = MapUtil.getStr(body, "password"); - if (StrUtil.isEmpty(password)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空"); - return; - } - - // 2.1 执行认证 - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); - if (result.isError()) { - log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg()); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg()); - return; - } - if (!BooleanUtil.isTrue(result.getData())) { - log.warn("[handle][认证失败,clientId: {}]", clientId); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败"); - return; - } - // 2.2 生成 Token - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); - Assert.notNull(deviceInfo, "设备信息不能为空"); - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - Assert.notBlank(token, "生成 token 不能为空"); - - // 3. 执行上线 - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(message, - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); - - // 4. 返回成功响应 - log.info("[handle][认证成功,productKey: {}, deviceName: {}]", - deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token)); - } catch (Exception e) { - log.error("[handle][认证处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java deleted file mode 100644 index 8ffbe4f67..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.Map; - -/** - * IoT 网关 CoAP 协议的【设备动态注册】处理器 - *

- * 用于直连设备/网关的一型一密动态注册,不需要认证 - * - * @author 芋道源码 - * @see 阿里云 - 一型一密 - * @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler - */ -@Slf4j -public class IotCoapRegisterHandler { - - private final IotDeviceCommonApi deviceApi; - - public IotCoapRegisterHandler() { - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - } - - /** - * 处理设备动态注册请求 - * - * @param exchange CoAP 交换对象 - */ - @SuppressWarnings("unchecked") - public void handle(CoapExchange exchange) { - try { - // 1.1 解析请求体 - byte[] payload = exchange.getRequestPayload(); - if (payload == null || payload.length == 0) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - Map body; - try { - body = JsonUtils.parseObject(new String(payload), Map.class); - } catch (Exception e) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误"); - return; - } - - // 1.2 解析参数 - String productKey = MapUtil.getStr(body, "productKey"); - if (StrUtil.isEmpty(productKey)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); - return; - } - String deviceName = MapUtil.getStr(body, "deviceName"); - if (StrUtil.isEmpty(deviceName)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); - return; - } - String productSecret = MapUtil.getStr(body, "productSecret"); - if (StrUtil.isEmpty(productSecret)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空"); - return; - } - - // 2. 调用动态注册 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() - .setProductKey(productKey) - .setDeviceName(deviceName) - .setProductSecret(productSecret); - CommonResult result = deviceApi.registerDevice(reqDTO); - if (result.isError()) { - log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]", - productKey, deviceName, result.getMsg()); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, - "设备动态注册失败:" + result.getMsg()); - return; - } - - // 3. 返回成功响应 - log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName); - IotCoapUtils.respondSuccess(exchange, result.getData()); - } catch (Exception e) { - log.error("[handle][设备动态注册处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java deleted file mode 100644 index d33eb464b..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.text.StrPool; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import java.util.List; - -/** - * IoT 网关 CoAP 协议的【上行】处理器 - * - * 处理设备通过 CoAP 协议发送的上行消息,包括: - * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post - * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post - * - * Token 通过自定义 CoAP Option 2088 携带 - * - * @author 芋道源码 - */ -@Slf4j -public class IotCoapUpstreamHandler { - - private final IotDeviceTokenService deviceTokenService; - private final IotDeviceMessageService deviceMessageService; - - public IotCoapUpstreamHandler() { - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - } - - /** - * 处理 CoAP 请求 - * - * @param exchange CoAP 交换对象 - * @param protocol 协议对象 - */ - public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) { - try { - // 1. 解析通用参数 - List uriPath = exchange.getRequestOptions().getUriPath(); - String productKey = CollUtil.get(uriPath, 2); - String deviceName = CollUtil.get(uriPath, 3); - byte[] payload = exchange.getRequestPayload(); - if (StrUtil.isEmpty(productKey)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空"); - return; - } - if (StrUtil.isEmpty(deviceName)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空"); - return; - } - if (ArrayUtil.isEmpty(payload)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空"); - return; - } - - // 2. 认证:从自定义 Option 获取 token - String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN); - if (StrUtil.isEmpty(token)) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空"); - return; - } - // 验证 token - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); - if (deviceInfo == null) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期"); - return; - } - // 验证设备信息匹配 - if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) - || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配"); - return; - } - - // 2.1 解析 method:deviceName 后面的路径,用 . 拼接 - // 路径格式:[topic, sys, productKey, deviceName, thing, property, post] - String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size())); - - // 2.2 解码消息 - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); - if (ObjUtil.notEqual(method, message.getMethod())) { - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配"); - return; - } - // 2.3 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId()); - - // 3. 返回成功响应 - IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId())); - } catch (Exception e) { - log.error("[handle][CoAP 请求处理异常]", e); - IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误"); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java deleted file mode 100644 index 9d5cdf3ff..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.coap.MediaTypeRegistry; -import org.eclipse.californium.core.coap.Option; -import org.eclipse.californium.core.server.resources.CoapExchange; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; - -/** - * IoT CoAP 协议工具类 - * - * @author 芋道源码 - */ -public class IotCoapUtils { - - /** - * 自定义 CoAP Option 编号,用于携带 Token - *

- * CoAP Option 范围 2048-65535 属于实验/自定义范围 - */ - public static final int OPTION_TOKEN = 2088; - - /** - * 返回成功响应 - * - * @param exchange CoAP 交换对象 - * @param data 响应数据 - */ - public static void respondSuccess(CoapExchange exchange, Object data) { - CommonResult result = CommonResult.success(data); - String json = JsonUtils.toJsonString(result); - exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON); - } - - /** - * 返回错误响应 - * - * @param exchange CoAP 交换对象 - * @param code CoAP 响应码 - * @param message 错误消息 - */ - public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) { - int errorCode = mapCoapCodeToErrorCode(code); - CommonResult result = CommonResult.error(errorCode, message); - String json = JsonUtils.toJsonString(result); - exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON); - } - - /** - * 从自定义 CoAP Option 中获取 Token - * - * @param exchange CoAP 交换对象 - * @param optionNumber Option 编号 - * @return Token 值,如果不存在则返回 null - */ - public static String getTokenFromOption(CoapExchange exchange, int optionNumber) { - Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(), - o -> o.getNumber() == optionNumber); - return option != null ? new String(option.getValue()) : null; - } - - /** - * 将 CoAP 响应码映射到业务错误码 - * - * @param code CoAP 响应码 - * @return 业务错误码 - */ - public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) { - if (code == CoAP.ResponseCode.BAD_REQUEST) { - return BAD_REQUEST.getCode(); - } else if (code == CoAP.ResponseCode.UNAUTHORIZED) { - return UNAUTHORIZED.getCode(); - } else if (code == CoAP.ResponseCode.FORBIDDEN) { - return FORBIDDEN.getCode(); - } else { - return INTERNAL_SERVER_ERROR.getCode(); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java deleted file mode 100644 index ce10cf76d..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java +++ /dev/null @@ -1,104 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; - -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler; -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 EMQX 认证事件协议服务 - *

- * 为 EMQX 提供 HTTP 接口服务,包括: - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 - * - * @author 芋道源码 - */ -@Slf4j -public class IotEmqxAuthEventProtocol { - - private final IotGatewayProperties.EmqxProperties emqxProperties; - - private final String serverId; - - private final Vertx vertx; - - private HttpServer httpServer; - - public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties, - Vertx vertx) { - this.emqxProperties = emqxProperties; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); - } - - @PostConstruct - public void start() { - try { - startHttpServer(); - log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - stopHttpServer(); - log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]"); - } - - /** - * 启动 HTTP 服务器 - */ - private void startHttpServer() { - int port = emqxProperties.getHttpPort(); - - // 1. 创建路由 - Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); - - // 2. 创建处理器,传入 serverId - IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId); - router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); - router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); - // TODO @haohao:/mqtt/acl 需要处理么? - // TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理 - - // 3. 启动 HTTP 服务器 - try { - httpServer = vertx.createHttpServer() - .requestHandler(router) - .listen(port) - .result(); - } catch (Exception e) { - log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e); - throw e; - } - } - - /** - * 停止 HTTP 服务器 - */ - private void stopHttpServer() { - if (httpServer == null) { - return; - } - - try { - httpServer.close().result(); - log.info("[stopHttpServer][HTTP 服务器已停止]"); - } catch (Exception e) { - log.error("[stopHttpServer][HTTP 服务器停止失败]", e); - } - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java new file mode 100644 index 000000000..bc039fe5c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxConfig.java @@ -0,0 +1,225 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * IoT EMQX 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotEmqxConfig { + + // ========== MQTT Client 配置(连接 EMQX Broker) ========== + + /** + * MQTT 服务器地址 + */ + @NotEmpty(message = "MQTT 服务器地址不能为空") + private String mqttHost; + + /** + * MQTT 服务器端口(默认:1883) + */ + @NotNull(message = "MQTT 服务器端口不能为空") + private Integer mqttPort = 1883; + + /** + * MQTT 用户名 + */ + @NotEmpty(message = "MQTT 用户名不能为空") + private String mqttUsername; + + /** + * MQTT 密码 + */ + @NotEmpty(message = "MQTT 密码不能为空") + private String mqttPassword; + + /** + * MQTT 客户端的 SSL 开关 + */ + @NotNull(message = "MQTT 是否开启 SSL 不能为空") + private Boolean mqttSsl = false; + + /** + * MQTT 客户端 ID + */ + @NotEmpty(message = "MQTT 客户端 ID 不能为空") + private String mqttClientId; + + /** + * MQTT 订阅的主题 + */ + @NotEmpty(message = "MQTT 主题不能为空") + private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; + + /** + * 默认 QoS 级别 + *

+ * 0 - 最多一次 + * 1 - 至少一次 + * 2 - 刚好一次 + */ + @NotNull(message = "MQTT QoS 不能为空") + @Min(value = 0, message = "MQTT QoS 不能小于 0") + @Max(value = 2, message = "MQTT QoS 不能大于 2") + private Integer mqttQos = 1; + + /** + * 连接超时时间(秒) + */ + @NotNull(message = "连接超时时间不能为空") + @Min(value = 1, message = "连接超时时间不能小于 1 秒") + private Integer connectTimeoutSeconds = 10; + + /** + * 重连延迟时间(毫秒) + */ + @NotNull(message = "重连延迟时间不能为空") + @Min(value = 0, message = "重连延迟时间不能小于 0 毫秒") + private Long reconnectDelayMs = 5000L; + + /** + * 是否启用 Clean Session (清理会话) + * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 + * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 + */ + @NotNull(message = "是否启用 Clean Session 不能为空") + private Boolean cleanSession = true; + + /** + * 心跳间隔(秒) + * 用于保持连接活性,及时发现网络中断。 + */ + @NotNull(message = "心跳间隔不能为空") + @Min(value = 1, message = "心跳间隔不能小于 1 秒") + private Integer keepAliveIntervalSeconds = 60; + + /** + * 最大未确认消息队列大小 + * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 + */ + @NotNull(message = "最大未确认消息队列大小不能为空") + @Min(value = 1, message = "最大未确认消息队列大小不能小于 1") + private Integer maxInflightQueue = 10000; + + /** + * 是否信任所有 SSL 证书 + * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! + * 在生产环境中,应设置为 false,并配置正确的信任库。 + */ + @NotNull(message = "是否信任所有 SSL 证书不能为空") + private Boolean trustAll = false; + + // ========== MQTT Will / SSL 高级配置 ========== + + /** + * 遗嘱消息配置 (用于网关异常下线时通知其他系统) + */ + @Valid + private Will will = new Will(); + + /** + * 高级 SSL/TLS 配置 (用于生产环境) + */ + @Valid + private Ssl sslOptions = new Ssl(); + + // ========== HTTP Hook 配置(网关提供给 EMQX 调用) ========== + + /** + * HTTP Hook 服务配置(用于 /mqtt/auth、/mqtt/event) + */ + @Valid + private Http http = new Http(); + + /** + * 遗嘱消息 (Last Will and Testament) + */ + @Data + public static class Will { + + /** + * 是否启用遗嘱消息 + */ + private boolean enabled = false; + /** + * 遗嘱消息主题 + */ + private String topic; + /** + * 遗嘱消息内容 + */ + private String payload; + /** + * 遗嘱消息 QoS 等级 + */ + @Min(value = 0, message = "遗嘱消息 QoS 不能小于 0") + @Max(value = 2, message = "遗嘱消息 QoS 不能大于 2") + private Integer qos = 1; + /** + * 遗嘱消息是否作为保留消息发布 + */ + private boolean retain = true; + + } + + /** + * 高级 SSL/TLS 配置 + */ + @Data + public static class Ssl { + + /** + * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 + */ + private String keyStorePath; + /** + * 密钥库密码 + */ + private String keyStorePassword; + /** + * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks + * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 + */ + private String trustStorePath; + /** + * 信任库密码 + */ + private String trustStorePassword; + + } + + /** + * HTTP Hook 服务 SSL 配置 + */ + @Data + public static class Http { + + /** + * 是否启用 SSL + */ + private Boolean sslEnabled = false; + + /** + * SSL 证书路径 + */ + private String sslCertPath; + + /** + * SSL 私钥路径 + */ + private String sslKeyPath; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java new file mode 100644 index 000000000..f110f64b4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java @@ -0,0 +1,532 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.JksOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * IoT 网关 EMQX 协议实现: + *

+ * 1. 提供 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event)给 EMQX 调用 + * 2. 通过 MQTT Client 订阅设备上行消息,并发布下行消息到 Broker + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * EMQX 配置 + */ + private final IotEmqxConfig emqxConfig; + /** + * 服务器 ID + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * HTTP Hook 服务器 + */ + private HttpServer httpServer; + + /** + * MQTT Client + */ + private volatile MqttClient mqttClient; + /** + * MQTT 重连定时器 ID + */ + private volatile Long reconnectTimerId; + + /** + * 上行消息处理器 + */ + private final IotEmqxUpstreamHandler upstreamHandler; + + /** + * 下行消息订阅者 + */ + private IotEmqxDownstreamSubscriber downstreamSubscriber; + + public IotEmqxProtocol(ProtocolProperties properties) { + Assert.notNull(properties, "协议实例配置不能为空"); + Assert.notNull(properties.getEmqx(), "EMQX 协议配置(emqx)不能为空"); + this.properties = properties; + this.emqxConfig = properties.getEmqx(); + Assert.notNull(emqxConfig.getConnectTimeoutSeconds(), + "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空"); + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + this.upstreamHandler = new IotEmqxUpstreamHandler(serverId); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.EMQX; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 和 下行消息订阅者 + this.vertx = Vertx.vertx(); + + try { + // 1.2 启动 HTTP Hook 服务 + startHttpServer(); + + // 1.3 启动 MQTT Client + startMqttClient(); + running = true; + log.info("[start][IoT EMQX 协议 {} 启动成功,hookPort:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus); + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e); + // 启动失败时,关闭资源 + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 先置为 false:避免 closeHandler 触发重连 + running = false; + stopMqttClientReconnectChecker(); + // 2.2 停止 MQTT Client + stopMqttClient(); + + // 2.3 停止 HTTP Hook 服务 + stopHttpServer(); + + // 2.4 关闭 Vertx + if (vertx != null) { + try { + vertx.close().toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS); + log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + + log.info("[stop][IoT EMQX 协议 {} 已停止]", getId()); + } + + // ======================================= HTTP Hook Server ======================================= + + /** + * 启动 HTTP Hook 服务(/mqtt/auth、/mqtt/acl、/mqtt/event) + */ + private void startHttpServer() { + // 1. 创建路由 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击 + + // 2. 创建处理器 + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this); + router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); + router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl); + router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); + + // 3. 启动 HTTP Server(支持 HTTPS) + IotEmqxConfig.Http httpConfig = emqxConfig.getHttp(); + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); + if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) { + Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空"); + Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空"); + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(httpConfig.getSslKeyPath()) + .setCertPath(httpConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + try { + httpServer = vertx.createHttpServer(options) + .requestHandler(router) + .listen() + .toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS); + log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]", + getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())); + } catch (Exception e) { + log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e); + throw new RuntimeException("HTTP Hook 服务启动失败", e); + } + } + + private void stopHttpServer() { + if (httpServer == null) { + return; + } + try { + httpServer.close().toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId()); + } catch (Exception e) { + log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e); + } finally { + httpServer = null; + } + } + + // ======================================= MQTT Client ====================================== + + private void startMqttClient() { + // 1.1 创建 MQTT Client + MqttClient client = createMqttClient(); + this.mqttClient = client; + // 1.2 连接 MQTT Broker + if (!connectMqttClient(client)) { + throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败"); + } + + // 2. 启动定时重连检查 + startMqttClientReconnectChecker(); + } + + private void stopMqttClient() { + MqttClient client = this.mqttClient; + this.mqttClient = null; // 先清理引用 + if (client == null) { + return; + } + + // 1. 批量取消订阅(仅在连接时) + if (client.isConnected()) { + List topicList = emqxConfig.getMqttTopics(); + if (CollUtil.isNotEmpty(topicList)) { + try { + client.unsubscribe(topicList).toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e); + } + } + } + + // 2. 断开 MQTT 连接 + try { + client.disconnect().toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e); + } + } + + // ======================================= MQTT 基础方法 ====================================== + + /** + * 创建 MQTT 客户端 + * + * @return 新创建的 MqttClient + */ + private MqttClient createMqttClient() { + // 1.1 基础配置 + MqttClientOptions options = new MqttClientOptions() + .setClientId(emqxConfig.getMqttClientId()) + .setUsername(emqxConfig.getMqttUsername()) + .setPassword(emqxConfig.getMqttPassword()) + .setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl())) + .setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession())) + .setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds()) + .setMaxInflightQueue(emqxConfig.getMaxInflightQueue()); + options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒 + options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll())); + // 1.2 配置遗嘱消息 + IotEmqxConfig.Will will = emqxConfig.getWill(); + if (will != null && will.isEnabled()) { + Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空"); + Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空"); + options.setWillFlag(true) + .setWillTopic(will.getTopic()) + .setWillMessageBytes(Buffer.buffer(will.getPayload())) + .setWillQoS(will.getQos()) + .setWillRetain(will.isRetain()); + } + // 1.3 配置高级 SSL/TLS(仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空) + IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions(); + if (Boolean.TRUE.equals(emqxConfig.getMqttSsl()) + && Boolean.FALSE.equals(emqxConfig.getTrustAll()) + && sslOptions != null) { + if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { + options.setTrustStoreOptions(new JksOptions() + .setPath(sslOptions.getTrustStorePath()) + .setPassword(sslOptions.getTrustStorePassword())); + } + if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { + options.setKeyStoreOptions(new JksOptions() + .setPath(sslOptions.getKeyStorePath()) + .setPassword(sslOptions.getKeyStorePassword())); + } + } + + // 2. 创建客户端 + return MqttClient.create(vertx, options); + } + + /** + * 连接 MQTT Broker(同步等待) + * + * @param client MQTT 客户端 + * @return 连接成功返回 true,失败返回 false + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private synchronized boolean connectMqttClient(MqttClient client) { + String host = emqxConfig.getMqttHost(); + int port = emqxConfig.getMqttPort(); + int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds(); + try { + // 1. 连接 Broker + client.connect(port, host).toCompletionStage().toCompletableFuture() + .get(timeoutSeconds, TimeUnit.SECONDS); + log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]", + getId(), host, port); + + // 2. 设置处理器 + setupMqttClientHandlers(client); + subscribeMqttClientTopics(client); + return true; + } catch (Exception e) { + log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e); + return false; + } + } + + /** + * 关闭 MQTT 客户端 + */ + private void closeMqttClient() { + MqttClient oldClient = this.mqttClient; + this.mqttClient = null; // 先清理引用 + if (oldClient == null) { + return; + } + // 尽力释放(无论是否连接都尝试 disconnect) + try { + oldClient.disconnect().toCompletionStage().toCompletableFuture() + .get(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } + } + + // ======================================= MQTT 重连机制 ====================================== + + /** + * 启动 MQTT Client 周期性重连检查器 + */ + private void startMqttClientReconnectChecker() { + long interval = emqxConfig.getReconnectDelayMs(); + this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> { + if (!running) { + return; + } + if (mqttClient != null && mqttClient.isConnected()) { + return; + } + log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId()); + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) + vertx.executeBlocking(() -> { + tryReconnectMqttClient(); + return null; + }); + }); + } + + /** + * 停止 MQTT Client 重连检查器 + */ + private void stopMqttClientReconnectChecker() { + if (reconnectTimerId != null && vertx != null) { + try { + vertx.cancelTimer(reconnectTimerId); + } catch (Exception ignored) { + } + reconnectTimerId = null; + } + } + + /** + * 尝试重连 MQTT Client + */ + private synchronized void tryReconnectMqttClient() { + // 1. 前置检查 + if (!running) { + return; + } + if (mqttClient != null && mqttClient.isConnected()) { + return; + } + + log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId()); + try { + // 2. 关闭旧客户端 + closeMqttClient(); + + // 3.1 创建新客户端 + MqttClient client = createMqttClient(); + this.mqttClient = client; + // 3.2 连接(失败只打印日志,等下次定时) + if (!connectMqttClient(client)) { + log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId()); + } + } catch (Exception e) { + log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e); + } + } + + // ======================================= MQTT Handler ====================================== + + /** + * 设置 MQTT Client 事件处理器 + */ + private void setupMqttClientHandlers(MqttClient client) { + // 1. 断开重连监听 + client.closeHandler(closeEvent -> { + if (!running) { + return; + } + log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId()); + // 用 executeBlocking 避免阻塞 event-loop(tryReconnectMqttClient 内部有同步等待) + vertx.executeBlocking(() -> { + tryReconnectMqttClient(); + return null; + }); + }); + + // 2. 异常处理 + client.exceptionHandler(exception -> + log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception)); + + // 3. 上行消息处理 + client.publishHandler(upstreamHandler::handle); + } + + /** + * 订阅 MQTT Client 主题(同步等待) + */ + private void subscribeMqttClientTopics(MqttClient client) { + List topicList = emqxConfig.getMqttTopics(); + if (!client.isConnected()) { + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId()); + return; + } + if (CollUtil.isEmpty(topicList)) { + log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId()); + return; + } + // 执行订阅 + Map topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic, + topic -> emqxConfig.getMqttQos()); + try { + client.subscribe(topics).toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS); + log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size()); + } catch (Exception e) { + log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e); + } + } + + /** + * 发布消息到 MQTT Broker + * + * @param topic 主题 + * @param payload 消息内容 + */ + public void publishMessage(String topic, byte[] payload) { + if (mqttClient == null || !mqttClient.isConnected()) { + log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId()); + return; + } + MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos()); + mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false) + .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e)); + } + + /** + * 延迟发布消息到 MQTT Broker + * + * @param topic 主题 + * @param payload 消息内容 + * @param delayMs 延迟时间(毫秒) + */ + public void publishDelayMessage(String topic, byte[] payload, long delayMs) { + vertx.setTimer(delayMs, id -> publishMessage(topic, payload)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java deleted file mode 100644 index a88815874..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java +++ /dev/null @@ -1,365 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.JksOptions; -import io.vertx.mqtt.MqttClient; -import io.vertx.mqtt.MqttClientOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * IoT 网关 EMQX 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotEmqxUpstreamProtocol { - - private final IotGatewayProperties.EmqxProperties emqxProperties; - - private volatile boolean isRunning = false; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private MqttClient mqttClient; - - private IotEmqxUpstreamHandler upstreamHandler; - - public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties, - Vertx vertx) { - this.emqxProperties = emqxProperties; - this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); - this.vertx = vertx; - } - - @PostConstruct - public void start() { - if (isRunning) { - return; - } - - try { - // 1. 启动 MQTT 客户端 - startMqttClient(); - - // 2. 标记服务为运行状态 - isRunning = true; - log.info("[start][IoT 网关 EMQX 协议启动成功]"); - } catch (Exception e) { - log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e); - stop(); - - // 异步关闭应用 - Thread shutdownThread = new Thread(() -> { - try { - // 确保日志输出完成,使用更优雅的方式 - log.error("[start][由于 MQTT 连接失败,正在关闭应用]"); - // 等待日志输出完成 - Thread.sleep(1000); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - log.warn("[start][应用关闭被中断]"); - } - System.exit(1); - }); - shutdownThread.setDaemon(true); - shutdownThread.setName("emergency-shutdown"); - shutdownThread.start(); - - throw e; - } - } - - @PreDestroy - public void stop() { - if (!isRunning) { - return; - } - - // 1. 停止 MQTT 客户端 - stopMqttClient(); - - // 2. 标记服务为停止状态 - isRunning = false; - log.info("[stop][IoT 网关 MQTT 协议服务已停止]"); - } - - /** - * 启动 MQTT 客户端 - */ - private void startMqttClient() { - try { - // 1. 初始化消息处理器 - this.upstreamHandler = new IotEmqxUpstreamHandler(this); - - // 2. 创建 MQTT 客户端 - createMqttClient(); - - // 3. 同步连接 MQTT Broker - connectMqttSync(); - } catch (Exception e) { - log.error("[startMqttClient][MQTT 客户端启动失败]", e); - throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e); - } - } - - /** - * 同步连接 MQTT Broker - */ - private void connectMqttSync() { - String host = emqxProperties.getMqttHost(); - int port = emqxProperties.getMqttPort(); - // 1. 连接 MQTT Broker - CountDownLatch latch = new CountDownLatch(1); - AtomicBoolean success = new AtomicBoolean(false); - mqttClient.connect(port, host, connectResult -> { - if (connectResult.succeeded()) { - log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port); - setupMqttHandlers(); - subscribeToTopics(); - success.set(true); - } else { - log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]", - host, port, connectResult.cause()); - } - latch.countDown(); - }); - - // 2. 等待连接结果 - try { - // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制 - boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS); - if (!awaitResult) { - log.error("[connectMqttSync][等待连接结果超时]"); - throw new RuntimeException("连接 MQTT Broker 超时"); - } - if (!success.get()) { - throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port)); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("[connectMqttSync][等待连接结果被中断]", e); - throw new RuntimeException("连接 MQTT Broker 被中断", e); - } - } - - /** - * 异步连接 MQTT Broker - */ - private void connectMqttAsync() { - String host = emqxProperties.getMqttHost(); - int port = emqxProperties.getMqttPort(); - mqttClient.connect(port, host, connectResult -> { - if (connectResult.succeeded()) { - log.info("[connectMqttAsync][MQTT 客户端重连成功]"); - setupMqttHandlers(); - subscribeToTopics(); - } else { - log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]", - host, port, connectResult.cause()); - log.warn("[connectMqttAsync][重连失败,将再次尝试]"); - reconnectWithDelay(); - } - }); - } - - /** - * 延迟重连 - */ - private void reconnectWithDelay() { - if (!isRunning) { - return; - } - if (mqttClient != null && mqttClient.isConnected()) { - return; - } - - long delay = emqxProperties.getReconnectDelayMs(); - log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay); - vertx.setTimer(delay, timerId -> { - if (!isRunning) { - return; - } - if (mqttClient != null && mqttClient.isConnected()) { - return; - } - - log.info("[reconnectWithDelay][开始重连 MQTT Broker]"); - try { - createMqttClient(); - connectMqttAsync(); - } catch (Exception e) { - log.error("[reconnectWithDelay][重连过程中发生异常]", e); - vertx.setTimer(delay, t -> reconnectWithDelay()); - } - }); - } - - /** - * 停止 MQTT 客户端 - */ - private void stopMqttClient() { - if (mqttClient == null) { - return; - } - try { - if (mqttClient.isConnected()) { - // 1. 取消订阅所有主题 - List topicList = emqxProperties.getMqttTopics(); - for (String topic : topicList) { - try { - mqttClient.unsubscribe(topic); - } catch (Exception e) { - log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); - } - } - - // 2. 断开 MQTT 客户端连接 - try { - CountDownLatch disconnectLatch = new CountDownLatch(1); - mqttClient.disconnect(ar -> disconnectLatch.countDown()); - if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) { - log.warn("[stopMqttClient][断开 MQTT 连接超时]"); - } - } catch (Exception e) { - log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e); - } - } - } catch (Exception e) { - log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e); - } finally { - mqttClient = null; - } - } - - /** - * 创建 MQTT 客户端 - */ - private void createMqttClient() { - // 1.1 创建基础配置 - MqttClientOptions options = (MqttClientOptions) new MqttClientOptions() - .setClientId(emqxProperties.getMqttClientId()) - .setUsername(emqxProperties.getMqttUsername()) - .setPassword(emqxProperties.getMqttPassword()) - .setSsl(emqxProperties.getMqttSsl()) - .setCleanSession(emqxProperties.getCleanSession()) - .setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds()) - .setMaxInflightQueue(emqxProperties.getMaxInflightQueue()) - .setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒 - .setTrustAll(emqxProperties.getTrustAll()); - // 1.2 配置遗嘱消息 - IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill(); - if (will.isEnabled()) { - Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空"); - Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空"); - options.setWillFlag(true) - .setWillTopic(will.getTopic()) - .setWillMessageBytes(Buffer.buffer(will.getPayload())) - .setWillQoS(will.getQos()) - .setWillRetain(will.isRetain()); - } - // 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效) - if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) { - IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions(); - if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { - options.setTrustStoreOptions(new JksOptions() - .setPath(sslOptions.getTrustStorePath()) - .setPassword(sslOptions.getTrustStorePassword())); - } - if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { - options.setKeyStoreOptions(new JksOptions() - .setPath(sslOptions.getKeyStorePath()) - .setPassword(sslOptions.getKeyStorePassword())); - } - } - // 1.4 安全警告日志 - if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) { - log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书(trustAll=true),这在生产环境中存在严重安全风险!]"); - } - - // 2. 创建客户端实例 - this.mqttClient = MqttClient.create(vertx, options); - } - - /** - * 设置 MQTT 处理器 - */ - private void setupMqttHandlers() { - // 1. 设置断开重连监听器 - mqttClient.closeHandler(closeEvent -> { - if (!isRunning) { - return; - } - log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); - reconnectWithDelay(); - }); - - // 2. 设置异常处理器 - mqttClient.exceptionHandler(exception -> - log.error("[exceptionHandler][MQTT 客户端异常]", exception)); - - // 3. 设置消息处理器 - mqttClient.publishHandler(upstreamHandler::handle); - } - - /** - * 订阅设备上行消息主题 - */ - private void subscribeToTopics() { - // 1. 校验 MQTT 客户端是否连接 - List topicList = emqxProperties.getMqttTopics(); - if (mqttClient == null || !mqttClient.isConnected()) { - log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]"); - return; - } - - // 2. 批量订阅所有主题 - Map topics = new HashMap<>(); - int qos = emqxProperties.getMqttQos(); - for (String topic : topicList) { - topics.put(topic, qos); - } - mqttClient.subscribe(topics, subscribeResult -> { - if (subscribeResult.succeeded()) { - log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size()); - } else { - log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]", - topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause()); - } - }); - } - - /** - * 发布消息到 MQTT Broker - * - * @param topic 主题 - * @param payload 消息内容 - */ - public void publishMessage(String topic, byte[] payload) { - if (mqttClient == null || !mqttClient.isConnected()) { - log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]"); - return; - } - MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos()); - mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java similarity index 88% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java index 06632b3e8..db5ea124e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java @@ -1,11 +1,11 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; @@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class IotEmqxDownstreamHandler { - private final IotEmqxUpstreamProtocol protocol; + private final IotEmqxProtocol protocol; private final IotDeviceService deviceService; private final IotDeviceMessageService deviceMessageService; - public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) { + public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) { this.protocol = protocol; this.deviceService = SpringUtil.getBean(IotDeviceService.class); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); @@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler { return; } // 2.2 构建载荷 - byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), + byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - // 2.3 发布消息 + + // 3. 发布消息 protocol.publishMessage(topic, payload); } @@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler { return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java new file mode 100644 index 000000000..e7e5de98d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 EMQX 订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { + + private final IotEmqxDownstreamHandler downstreamHandler; + + public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java similarity index 52% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java index 6b6694fd9..ccc7a2b7e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java @@ -1,25 +1,35 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import lombok.extern.slf4j.Slf4j; +import java.util.Locale; + /** * IoT 网关 EMQX 认证事件处理器 *

* 为 EMQX 提供 HTTP 接口服务,包括: - * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 - * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 + * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)} + * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)} + * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)} + * 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)} * * @author 芋道源码 */ @@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler { private static final String RESULT_IGNORE = "ignore"; /** - * EMQX 事件类型常量 + * EMQX 事件类型常量 - 客户端连接 */ private static final String EVENT_CLIENT_CONNECTED = "client.connected"; + /** + * EMQX 事件类型常量 - 客户端断开连接 + */ private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; + /** + * 认证类型标识 - 设备注册 + */ + private static final String AUTH_TYPE_REGISTER = "|authType=register|"; + private final String serverId; - private final IotDeviceMessageService deviceMessageService; + private final IotEmqxProtocol protocol; + private final IotDeviceMessageService deviceMessageService; private final IotDeviceCommonApi deviceApi; - public IotEmqxAuthEventHandler(String serverId) { + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) { this.serverId = serverId; + this.protocol = protocol; this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); } + // ========== 认证处理 ========== + /** * EMQX 认证接口 */ public void handleAuth(RoutingContext context) { + JsonObject body = null; try { // 1. 参数校验 - JsonObject body = parseRequestBody(context); + body = parseRequestBody(context); if (body == null) { return; } @@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler { return; } - // 2. 执行认证 + // 2.1 情况一:判断是否为注册请求 + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) { + handleDeviceRegister(context, username, password); + return; + } + + // 2.2 情况二:执行认证 boolean authResult = handleDeviceAuth(clientId, username, password); log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); if (authResult) { @@ -91,11 +120,179 @@ public class IotEmqxAuthEventHandler { sendAuthResponse(context, RESULT_DENY); } } catch (Exception e) { - log.error("[handleAuth][设备认证异常]", e); + log.error("[handleAuth][设备认证异常][body={}]", body, e); sendAuthResponse(context, RESULT_IGNORE); } } + /** + * 解析认证接口请求体 + *

+ * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) + * + * @param context 路由上下文 + * @return 请求体JSON对象,解析失败时返回null + */ + private JsonObject parseRequestBody(RoutingContext context) { + try { + JsonObject body = context.body().asJsonObject(); + if (body == null) { + log.info("[parseRequestBody][请求体为空]"); + sendAuthResponse(context, RESULT_IGNORE); + return null; + } + return body; + } catch (Exception e) { + log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); + sendAuthResponse(context, RESULT_IGNORE); + return null; + } + } + + /** + * 执行设备认证 + * + * @param clientId 客户端ID + * @param username 用户名 + * @param password 密码 + * @return 认证是否成功 + */ + private boolean handleDeviceAuth(String clientId, String username, String password) { + try { + CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() + .setClientId(clientId).setUsername(username).setPassword(password)); + result.checkError(); + return BooleanUtil.isTrue(result.getData()); + } catch (Exception e) { + log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); + throw e; + } + } + + /** + * 发送 EMQX 认证响应 + * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 + * + * @param context 路由上下文 + * @param result 认证结果:allow、deny、ignore + */ + private void sendAuthResponse(RoutingContext context, String result) { + // 构建符合 EMQX 官方规范的响应 + JsonObject response = new JsonObject() + .put("result", result) + .put("is_superuser", false); + // 可以根据业务需求添加客户端属性 + // response.put("client_attrs", new JsonObject().put("role", "device")); + // 可以添加认证过期时间(可选) + // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); + + // 回复响应 + context.response() + .setStatusCode(SUCCESS_STATUS_CODE) + .putHeader("Content-Type", "application/json; charset=utf-8") + .end(response.encode()); + } + + // ========== ACL 处理 ========== + + /** + * EMQX ACL 接口 + *

+ * 用于 EMQX 的 HTTP ACL 插件校验设备的 publish/subscribe 权限。 + * 若请求参数无法识别,则返回 ignore 交给 EMQX 自身 ACL 规则处理。 + */ + public void handleAcl(RoutingContext context) { + JsonObject body = null; + try { + // 1.1 解析请求体 + body = parseRequestBody(context); + if (body == null) { + return; + } + String username = body.getString("username"); + String topic = body.getString("topic"); + if (StrUtil.hasBlank(username, topic)) { + log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic); + sendAuthResponse(context, RESULT_IGNORE); + return; + } + // 1.2 解析设备身份 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + if (deviceInfo == null) { + sendAuthResponse(context, RESULT_IGNORE); + return; + } + // 1.3 解析 ACL 动作(兼容多种 EMQX 版本/插件字段) + Boolean subscribe = parseAclSubscribeFlag(body); + if (subscribe == null) { + sendAuthResponse(context, RESULT_IGNORE); + return; + } + + // 2. 执行 ACL 校验 + boolean allowed = subscribe + ? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()) + : IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY); + } catch (Exception e) { + log.error("[handleAcl][ACL 处理失败][body={}]", body, e); + sendAuthResponse(context, RESULT_IGNORE); + } + } + + /** + * 解析 ACL 动作类型:订阅/发布 + * + * @param body ACL 请求体 + * @return true 订阅;false 发布;null 不识别 + */ + private static Boolean parseAclSubscribeFlag(JsonObject body) { + // 1. action 字段(常见为 publish/subscribe) + String action = body.getString("action"); + if (StrUtil.isNotBlank(action)) { + String lower = action.toLowerCase(Locale.ROOT); + if (lower.contains("sub")) { + return true; + } + if (lower.contains("pub")) { + return false; + } + } + + // 2. access 字段:可能是数字或字符串 + Integer access = body.getInteger("access"); + if (access != null) { + if (access == 1) { + return true; + } + if (access == 2) { + return false; + } + } + String accessText = body.getString("access"); + if (StrUtil.isNotBlank(accessText)) { + String lower = accessText.toLowerCase(Locale.ROOT); + if (lower.contains("sub")) { + return true; + } + if (lower.contains("pub")) { + return false; + } + if (StrUtil.isNumeric(accessText)) { + int value = Integer.parseInt(accessText); + if (value == 1) { + return true; + } + if (value == 2) { + return false; + } + } + } + return null; + } + + // ========== 事件处理 ========== + /** * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 * 支持的事件类型:client.connected、client.disconnected 等 @@ -124,58 +321,15 @@ public class IotEmqxAuthEventHandler { break; } - // EMQX Webhook 只需要 200 状态码,无需响应体 + // 3. EMQX Webhook 只需要 200 状态码,无需响应体 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); } catch (Exception e) { - log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e); - // 即使处理失败,也返回 200 避免EMQX重试 + log.error("[handleEvent][事件处理失败][body={}]", body, e); + // 即使处理失败,也返回 200 避免 EMQX 重试 context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); } } - /** - * 处理客户端连接事件 - */ - private void handleClientConnected(JsonObject body) { - String username = body.getString("username"); - log.info("[handleClientConnected][设备上线: {}]", username); - handleDeviceStateChange(username, true); - } - - /** - * 处理客户端断开连接事件 - */ - private void handleClientDisconnected(JsonObject body) { - String username = body.getString("username"); - String reason = body.getString("reason"); - log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); - handleDeviceStateChange(username, false); - } - - /** - * 解析认证接口请求体 - *

- * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段) - * - * @param context 路由上下文 - * @return 请求体JSON对象,解析失败时返回null - */ - private JsonObject parseRequestBody(RoutingContext context) { - try { - JsonObject body = context.body().asJsonObject(); - if (body == null) { - log.info("[parseRequestBody][请求体为空]"); - sendAuthResponse(context, RESULT_IGNORE); - return null; - } - return body; - } catch (Exception e) { - log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); - sendAuthResponse(context, RESULT_IGNORE); - return null; - } - } - /** * 解析事件接口请求体 *

@@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler { } /** - * 执行设备认证 - * - * @param clientId 客户端ID - * @param username 用户名 - * @param password 密码 - * @return 认证是否成功 + * 处理客户端连接事件 */ - private boolean handleDeviceAuth(String clientId, String username, String password) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); - throw e; - } + private void handleClientConnected(JsonObject body) { + String username = body.getString("username"); + log.info("[handleClientConnected][设备上线: {}]", username); + handleDeviceStateChange(username, true); + } + + /** + * 处理客户端断开连接事件 + */ + private void handleClientDisconnected(JsonObject body) { + String username = body.getString("username"); + String reason = body.getString("reason"); + log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); + handleDeviceStateChange(username, false); } /** @@ -247,29 +400,74 @@ public class IotEmqxAuthEventHandler { } } + // ========= 注册处理 ========= + /** - * 发送 EMQX 认证响应 - * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 + * 处理设备注册请求(一型一密) * - * @param context 路由上下文 - * @param result 认证结果:allow、deny、ignore + * @param context 路由上下文 + * @param username 用户名 + * @param password 密码(签名) */ - private void sendAuthResponse(RoutingContext context, String result) { - // 构建符合 EMQX 官方规范的响应 - JsonObject response = new JsonObject() - .put("result", result) - .put("is_superuser", false); + private void handleDeviceRegister(RoutingContext context, String username, String password) { + try { + // 1. 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + if (deviceInfo == null) { + log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username); + sendAuthResponse(context, RESULT_DENY); + return; + } - // 可以根据业务需求添加客户端属性 - // response.put("client_attrs", new JsonObject().put("role", "device")); + // 2. 调用注册 API + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO() + .setProductKey(deviceInfo.getProductKey()) + .setDeviceName(deviceInfo.getDeviceName()) + .setSign(password); + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); - // 可以添加认证过期时间(可选) - // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); + // 3. 允许连接 + log.info("[handleDeviceRegister][设备注册成功: {}]", username); + sendAuthResponse(context, RESULT_ALLOW); - context.response() - .setStatusCode(SUCCESS_STATUS_CODE) - .putHeader("Content-Type", "application/json; charset=utf-8") - .end(response.encode()); + // 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅) + sendRegisterResultMessage(username, result.getData()); + } catch (Exception e) { + log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage()); + sendAuthResponse(context, RESULT_DENY); + } } -} \ No newline at end of file + /** + * 发送注册结果消息给设备 + *

+ * 注意:延迟 5 秒发送,等待设备连接成功并完成订阅。 + * + * @param username 用户名 + * @param result 注册结果 + */ + @SuppressWarnings("DataFlowIssue") + private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) { + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "设备信息不能为空"); + try { + // 1.1 构建响应消息 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null); + // 1.2 序列化消息 + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, + cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON); + // 1.3 构建响应主题 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true); + + // 2. 构建响应主题,并延迟发布(等待设备连接成功并完成订阅) + protocol.publishDelayMessage(replyTopic, encodedData, 5000); + log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic); + } catch (Exception e) { + log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java similarity index 61% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java index 81d8cbb13..45c94818c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java @@ -1,10 +1,11 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; import io.vertx.mqtt.messages.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; @@ -20,41 +21,42 @@ public class IotEmqxUpstreamHandler { private final String serverId; - public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) { + public IotEmqxUpstreamHandler(String serverId) { this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); - this.serverId = protocol.getServerId(); + this.serverId = serverId; } /** * 处理 MQTT 发布消息 */ public void handle(MqttPublishMessage mqttMessage) { - log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); + log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); String topic = mqttMessage.topicName(); byte[] payload = mqttMessage.payload().getBytes(); try { // 1. 解析主题,一次性获取所有信息 String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { + String productKey = ArrayUtil.get(topicParts, 2); + String deviceName = ArrayUtil.get(topicParts, 3); + if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) { log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); return; } - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - - // 3. 解码消息 - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); + // 2.1 反序列化消息 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); if (message == null) { log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); return; } + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) + IotMqttTopicUtils.normalizeReplyMethod(message); - // 4. 发送消息到队列 + // 3. 发送消息到队列 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); } catch (Exception e) { log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); } } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java new file mode 100644 index 000000000..bc7f4dc8c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpConfig.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import lombok.Data; + +/** + * IoT HTTP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotHttpConfig { + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java deleted file mode 100644 index 585bbdd30..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 HTTP 订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotHttpDownstreamSubscriber implements IotMessageSubscriber { - - private final IotHttpUpstreamProtocol protocol; - - private final IotMessageBus messageBus; - - @PostConstruct - public void init() { - messageBus.register(this); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - log.info("[onMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java new file mode 100644 index 000000000..f6c9bdc90 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java @@ -0,0 +1,176 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterSubHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpUpstreamHandler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT HTTP 协议实现 + *

+ * 基于 Vert.x 实现 HTTP 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotHttpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * HTTP 服务器 + */ + private HttpServer httpServer; + + /** + * 下行消息订阅者 + */ + private IotHttpDownstreamSubscriber downstreamSubscriber; + + public IotHttpProtocol(ProtocolProperties properties) { + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.HTTP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT HTTP 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建路由 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); + + // 1.3 创建处理器,添加路由处理器 + IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); + router.post(IotHttpAuthHandler.PATH).handler(authHandler); + IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler(); + router.post(IotHttpRegisterHandler.PATH).handler(registerHandler); + IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(); + router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler); + IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); + router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); + + // 1.4 启动 HTTP 服务器 + HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort()); + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); + options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + try { + httpServer = vertx.createHttpServer(options) + .requestHandler(router) + .listen() + .result(); + running = true; + log.info("[start][IoT HTTP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus); + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT HTTP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT HTTP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 关闭 HTTP 服务器 + if (httpServer != null) { + try { + httpServer.close().result(); + log.info("[stop][IoT HTTP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT HTTP 协议 {} 服务器停止失败]", getId(), e); + } + httpServer = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT HTTP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT HTTP 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT HTTP 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java deleted file mode 100644 index 54cb2da1f..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java +++ /dev/null @@ -1,91 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http; - -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 HTTP 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotHttpUpstreamProtocol { - - private final IotGatewayProperties.HttpProperties httpProperties; - - private final Vertx vertx; - - private HttpServer httpServer; - - @Getter - private final String serverId; - - public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) { - this.httpProperties = httpProperties; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort()); - } - - @PostConstruct - public void start() { - // 创建路由 - Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); - - // 创建处理器,添加路由处理器 - IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); - router.post(IotHttpAuthHandler.PATH).handler(authHandler); - IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler(); - router.post(IotHttpRegisterHandler.PATH).handler(registerHandler); - IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(); - router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler); - IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); - router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); - - // 启动 HTTP 服务器 - HttpServerOptions options = new HttpServerOptions() - .setPort(httpProperties.getServerPort()); - if (Boolean.TRUE.equals(httpProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions().setKeyPath(httpProperties.getSslKeyPath()) - .setCertPath(httpProperties.getSslCertPath()); - options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - try { - httpServer = vertx.createHttpServer(options) - .requestHandler(router) - .listen() - .result(); - log.info("[start][IoT 网关 HTTP 协议启动成功,端口:{}]", httpProperties.getServerPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 HTTP 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (httpServer != null) { - try { - httpServer.close().result(); - log.info("[stop][IoT 网关 HTTP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 HTTP 协议停止失败]", e); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java new file mode 100644 index 000000000..8f097c0b6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 HTTP 订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ + +@Slf4j +public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { + + public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) { + super(protocol, messageBus); + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + log.info("[handleMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java similarity index 72% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java index 850fde187..c403ee973 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAbstractHandler.java @@ -1,6 +1,7 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; @@ -13,12 +14,10 @@ import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; import io.vertx.core.Handler; import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; @@ -27,7 +26,6 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public abstract class IotHttpAbstractHandler implements Handler { @@ -43,15 +41,31 @@ public abstract class IotHttpAbstractHandler implements Handler CommonResult result = handle0(context); writeResponse(context, result); } catch (ServiceException e) { + // 已知异常,返回对应的错误码和错误信息 writeResponse(context, CommonResult.error(e.getCode(), e.getMessage())); + } catch (IllegalArgumentException e) { + // 参数校验异常,返回 400 错误 + writeResponse(context, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage())); } catch (Exception e) { + // 其他未知异常,返回 500 错误 log.error("[handle][path({}) 处理异常]", context.request().path(), e); writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR)); } } + /** + * 处理 HTTP 请求(子类实现) + * + * @param context RoutingContext 对象 + * @return 处理结果 + */ protected abstract CommonResult handle0(RoutingContext context); + /** + * 前置处理:认证等 + * + * @param context RoutingContext 对象 + */ private void beforeHandle(RoutingContext context) { // 如果不需要认证,则不走前置处理 String path = context.request().path(); @@ -83,12 +97,26 @@ public abstract class IotHttpAbstractHandler implements Handler } } + // ========== 序列化相关方法 ========== + + protected static T deserializeRequest(RoutingContext context, Class clazz) { + byte[] body = context.body().buffer() != null ? context.body().buffer().getBytes() : null; + if (ArrayUtil.isEmpty(body)) { + throw invalidParamException("请求体不能为空"); + } + return JsonUtils.parseObject(body, clazz); + } + + private static String serializeResponse(Object data) { + return JsonUtils.toJsonString(data); + } + @SuppressWarnings("deprecation") - public static void writeResponse(RoutingContext context, Object data) { + public static void writeResponse(RoutingContext context, CommonResult data) { context.response() .setStatusCode(200) .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) - .end(JsonUtils.toJsonString(data)); + .end(serializeResponse(data)); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java similarity index 66% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java index 148756ca8..21aa5a8fb 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpAuthHandler.java @@ -1,23 +1,20 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; @@ -32,7 +29,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { public static final String PATH = "/auth"; - private final IotHttpUpstreamProtocol protocol; + private final String serverId; private final IotDeviceTokenService deviceTokenService; @@ -40,42 +37,31 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { private final IotDeviceMessageService deviceMessageService; - public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) { - this.protocol = protocol; + public IotHttpAuthHandler(IotHttpProtocol protocol) { + this.serverId = protocol.getServerId(); this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); } @Override + @SuppressWarnings("DuplicatedCode") public CommonResult handle0(RoutingContext context) { // 1. 解析参数 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - String clientId = body.getString("clientId"); - if (StrUtil.isEmpty(clientId)) { - throw invalidParamException("clientId 不能为空"); - } - String username = body.getString("username"); - if (StrUtil.isEmpty(username)) { - throw invalidParamException("username 不能为空"); - } - String password = body.getString("password"); - if (StrUtil.isEmpty(password)) { - throw invalidParamException("password 不能为空"); - } + IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class); + Assert.notNull(request, "请求参数不能为空"); + Assert.notBlank(request.getClientId(), "clientId 不能为空"); + Assert.notBlank(request.getUsername(), "username 不能为空"); + Assert.notBlank(request.getPassword(), "password 不能为空"); // 2.1 执行认证 - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(clientId).setUsername(username).setPassword(password)); + CommonResult result = deviceApi.authDevice(request); result.checkError(); - if (!BooleanUtil.isTrue(result.getData())) { + if (BooleanUtil.isFalse(result.getData())) { throw exception(DEVICE_AUTH_FAIL); } // 2.2 生成 Token - IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username); + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername()); Assert.notNull(deviceInfo, "设备信息不能为空"); String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); Assert.notBlank(token, "生成 token 不能为空位"); @@ -83,7 +69,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { // 3. 执行上线 IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); deviceMessageService.sendDeviceMessage(message, - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); // 构建响应数据 return success(MapUtil.of("token", token)); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java similarity index 54% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java index 51459dfa2..df010f988 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java @@ -1,15 +1,13 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** @@ -33,27 +31,14 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler { @Override public CommonResult handle0(RoutingContext context) { // 1. 解析参数 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - String productKey = body.getString("productKey"); - if (StrUtil.isEmpty(productKey)) { - throw invalidParamException("productKey 不能为空"); - } - String deviceName = body.getString("deviceName"); - if (StrUtil.isEmpty(deviceName)) { - throw invalidParamException("deviceName 不能为空"); - } - String productSecret = body.getString("productSecret"); - if (StrUtil.isEmpty(productSecret)) { - throw invalidParamException("productSecret 不能为空"); - } + IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class); + Assert.notNull(request, "请求参数不能为空"); + Assert.notBlank(request.getProductKey(), "productKey 不能为空"); + Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(request.getSign(), "sign 不能为空"); // 2. 调用动态注册 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() - .setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret); - CommonResult result = deviceApi.registerDevice(reqDTO); + CommonResult result = deviceApi.registerDevice(request); result.checkError(); // 3. 返回结果 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java similarity index 61% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java index 32a6144b7..46932204d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterSubHandler.java @@ -1,17 +1,17 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; +import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; +import lombok.Data; import java.util.List; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** @@ -39,29 +39,31 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler { @Override public CommonResult handle0(RoutingContext context) { - // 1. 解析通用参数 + // 1.1 解析通用参数 String productKey = context.pathParam("productKey"); String deviceName = context.pathParam("deviceName"); + // 1.2 解析子设备列表 + SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class); + Assert.notNull(request, "请求参数不能为空"); + Assert.notEmpty(request.getParams(), "params 不能为空"); - // 2. 解析子设备列表 - JsonObject body = context.body().asJsonObject(); - if (body == null) { - throw invalidParamException("请求体不能为空"); - } - if (body.getJsonArray("params") == null) { - throw invalidParamException("params 不能为空"); - } - List subDevices = JsonUtils.parseArray( - body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class); - - // 3. 调用子设备动态注册 + // 2. 调用子设备动态注册 IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO() - .setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices); + .setGatewayProductKey(productKey) + .setGatewayDeviceName(deviceName) + .setSubDevices(request.getParams()); CommonResult> result = deviceApi.registerSubDevices(reqDTO); result.checkError(); - // 4. 返回结果 + // 3. 返回结果 return success(result.getData()); } + @Data + public static class SubDeviceRegisterRequest { + + private List params; + + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java similarity index 62% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java index 5289e03a1..aa408dc79 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpUpstreamHandler.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; @@ -6,55 +6,47 @@ import cn.hutool.core.text.StrPool; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; - /** * IoT 网关 HTTP 协议的【上行】处理器 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotHttpUpstreamHandler extends IotHttpAbstractHandler { public static final String PATH = "/topic/sys/:productKey/:deviceName/*"; - private final IotHttpUpstreamProtocol protocol; + private final String serverId; private final IotDeviceMessageService deviceMessageService; - public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) { - this.protocol = protocol; + public IotHttpUpstreamHandler(IotHttpProtocol protocol) { + this.serverId = protocol.getServerId(); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); } @Override protected CommonResult handle0(RoutingContext context) { - // 1. 解析通用参数 + // 1.1 解析通用参数 String productKey = context.pathParam("productKey"); String deviceName = context.pathParam("deviceName"); String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT); - - // 2.1 解析消息 - if (context.body().buffer() == null) { - throw invalidParamException("请求体不能为空"); - } - byte[] bytes = context.body().buffer().getBytes(); - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, - productKey, deviceName); + // 1.2 根据 Content-Type 反序列化消息 + IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class); + Assert.notNull(message, "请求参数不能为空"); Assert.equals(method, message.getMethod(), "method 不匹配"); - // 2.2 发送消息 + + // 2. 发送消息 deviceMessageService.sendDeviceMessage(message, - productKey, deviceName, protocol.getServerId()); + productKey, deviceName, serverId); // 3. 返回结果 return CommonResult.success(MapUtil.of("messageId", message.getId())); } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java new file mode 100644 index 000000000..e62f85fcf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java @@ -0,0 +1,278 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import io.vertx.core.Vertx; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * Modbus 轮询调度器基类 + *

+ * 封装通用的定时器管理、per-device 请求队列限速逻辑。 + * 子类只需实现 {@link #pollPoint(Long, Long)} 定义具体的轮询动作。 + *

+ * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractIotModbusPollScheduler { + + protected final Vertx vertx; + + /** + * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 + */ + private static final long MIN_REQUEST_INTERVAL = 1000; + /** + * 每个设备请求队列的最大长度,超出时丢弃最旧请求 + */ + private static final int MAX_QUEUE_SIZE = 1000; + + /** + * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) + */ + private final Map> devicePointTimers = new ConcurrentHashMap<>(); + + /** + * per-device 请求队列:deviceId -> 待执行请求队列 + */ + private final Map> deviceRequestQueues = new ConcurrentHashMap<>(); + /** + * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs + */ + private final Map deviceLastRequestTime = new ConcurrentHashMap<>(); + /** + * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待 + */ + private final Map deviceDelayTimerActive = new ConcurrentHashMap<>(); + + protected AbstractIotModbusPollScheduler(Vertx vertx) { + this.vertx = vertx; + } + + /** + * 点位定时器信息 + */ + @Data + @AllArgsConstructor + private static class PointTimerInfo { + + /** + * Vert.x 定时器 ID + */ + private Long timerId; + /** + * 轮询间隔(用于判断是否需要更新定时器) + */ + private Integer pollInterval; + + } + + // ========== 轮询管理 ========== + + /** + * 更新轮询任务(增量更新) + * + * 1. 【删除】点位:停止对应的轮询定时器 + * 2. 【新增】点位:创建对应的轮询定时器 + * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 + * 【修改】其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point) + */ + public void updatePolling(IotModbusDeviceConfigRespDTO config) { + Long deviceId = config.getDeviceId(); + List newPoints = config.getPoints(); + Map currentTimers = devicePointTimers + .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); + // 1.1 计算新配置中的点位 ID 集合 + Set newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); + // 1.2 计算删除的点位 ID 集合 + Set removedPointIds = new HashSet<>(currentTimers.keySet()); + removedPointIds.removeAll(newPointIds); + + // 2. 处理删除的点位:停止不再存在的定时器 + for (Long pointId : removedPointIds) { + PointTimerInfo timerInfo = currentTimers.remove(pointId); + if (timerInfo != null) { + vertx.cancelTimer(timerInfo.getTimerId()); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId); + } + } + + // 3. 处理新增和修改的点位 + if (CollUtil.isEmpty(newPoints)) { + return; + } + for (IotModbusPointRespDTO point : newPoints) { + Long pointId = point.getId(); + Integer newPollInterval = point.getPollInterval(); + PointTimerInfo existingTimer = currentTimers.get(pointId); + // 3.1 新增点位:创建定时器 + if (existingTimer == null) { + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); + if (timerId != null) { + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", + deviceId, pointId, newPollInterval); + } + } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { + // 3.2 pollInterval 变化:重建定时器 + vertx.cancelTimer(existingTimer.getTimerId()); + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); + if (timerId != null) { + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); + log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", + deviceId, pointId, existingTimer.getPollInterval(), newPollInterval); + } else { + currentTimers.remove(pointId); + } + } + // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置 + } + } + + /** + * 创建轮询定时器 + */ + private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) { + if (pollInterval == null || pollInterval <= 0) { + return null; + } + return vertx.setPeriodic(pollInterval, timerId -> { + try { + submitPollRequest(deviceId, pointId); + } catch (Exception e) { + log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e); + } + }); + } + + // ========== 请求队列(per-device 限速) ========== + + /** + * 提交轮询请求到设备请求队列(保证同设备请求间隔) + */ + private void submitPollRequest(Long deviceId, Long pointId) { + // 1. 【重要】将请求添加到设备的请求队列 + Queue queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); + while (queue.size() >= MAX_QUEUE_SIZE) { + // 超出上限时,丢弃最旧的请求 + queue.poll(); + log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE); + } + queue.offer(() -> pollPoint(deviceId, pointId)); + + // 2. 处理设备请求队列(如果没有延迟 timer 在等待) + processDeviceQueue(deviceId); + } + + /** + * 处理设备请求队列 + */ + private void processDeviceQueue(Long deviceId) { + Queue queue = deviceRequestQueues.get(deviceId); + if (CollUtil.isEmpty(queue)) { + return; + } + // 检查是否已有延迟 timer 在等待 + if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) { + return; + } + + // 不满足间隔要求,延迟执行 + long now = System.currentTimeMillis(); + long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L); + long elapsed = now - lastTime; + if (elapsed < MIN_REQUEST_INTERVAL) { + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed); + return; + } + + // 满足间隔要求,立即执行 + Runnable task = queue.poll(); + if (task == null) { + return; + } + deviceLastRequestTime.put(deviceId, now); + task.run(); + // 继续处理队列中的下一个(如果有的话,需要延迟) + if (CollUtil.isNotEmpty(queue)) { + scheduleNextRequest(deviceId); + } + } + + private void scheduleNextRequest(Long deviceId) { + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL); + } + + private void scheduleNextRequest(Long deviceId, long delayMs) { + deviceDelayTimerActive.put(deviceId, true); + vertx.setTimer(delayMs, id -> { + deviceDelayTimerActive.put(deviceId, false); + Queue queue = deviceRequestQueues.get(deviceId); + if (CollUtil.isEmpty(queue)) { + return; + } + + // 满足间隔要求,立即执行 + Runnable task = queue.poll(); + if (task == null) { + return; + } + deviceLastRequestTime.put(deviceId, System.currentTimeMillis()); + task.run(); + // 继续处理队列中的下一个(如果有的话,需要延迟) + if (CollUtil.isNotEmpty(queue)) { + scheduleNextRequest(deviceId); + } + }); + } + + // ========== 轮询执行 ========== + + /** + * 轮询单个点位(子类实现具体的读取逻辑) + * + * @param deviceId 设备 ID + * @param pointId 点位 ID + */ + protected abstract void pollPoint(Long deviceId, Long pointId); + + // ========== 停止 ========== + + /** + * 停止设备的轮询 + */ + public void stopPolling(Long deviceId) { + Map timers = devicePointTimers.remove(deviceId); + if (CollUtil.isEmpty(timers)) { + return; + } + for (PointTimerInfo timerInfo : timers.values()) { + vertx.cancelTimer(timerInfo.getTimerId()); + } + // 清理请求队列 + deviceRequestQueues.remove(deviceId); + deviceLastRequestTime.remove(deviceId); + deviceDelayTimerActive.remove(deviceId); + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); + } + + /** + * 停止所有轮询 + */ + public void stopAll() { + for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) { + stopPolling(deviceId); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java new file mode 100644 index 000000000..312e796df --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java @@ -0,0 +1,557 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * IoT Modbus 协议工具类 + *

+ * 提供 Modbus 协议全链路能力: + *

    + *
  • 协议常量:功能码(FC01~FC16)、异常掩码等
  • + *
  • 功能码判断:读/写/异常分类、可写判断、写功能码映射
  • + *
  • CRC-16/MODBUS 计算和校验
  • + *
  • 数据转换:原始值 ↔ 物模型属性值({@link #convertToPropertyValue} / {@link #convertToRawValues})
  • + *
  • 帧值提取:从 Modbus 帧提取寄存器/线圈值({@link #extractValues})
  • + *
  • 点位查找({@link #findPoint})
  • + *
+ * + * @author 芋道源码 + */ +@UtilityClass +@Slf4j +public class IotModbusCommonUtils { + + /** FC01: 读线圈 */ + public static final int FC_READ_COILS = 1; + /** FC02: 读离散输入 */ + public static final int FC_READ_DISCRETE_INPUTS = 2; + /** FC03: 读保持寄存器 */ + public static final int FC_READ_HOLDING_REGISTERS = 3; + /** FC04: 读输入寄存器 */ + public static final int FC_READ_INPUT_REGISTERS = 4; + + /** FC05: 写单个线圈 */ + public static final int FC_WRITE_SINGLE_COIL = 5; + /** FC06: 写单个寄存器 */ + public static final int FC_WRITE_SINGLE_REGISTER = 6; + /** FC15: 写多个线圈 */ + public static final int FC_WRITE_MULTIPLE_COILS = 15; + /** FC16: 写多个寄存器 */ + public static final int FC_WRITE_MULTIPLE_REGISTERS = 16; + + /** + * 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应 + * 例如:请求 FC=0x03,异常响应 FC=0x83(0x03 | 0x80) + */ + public static final int FC_EXCEPTION_MASK = 0x80; + + /** + * 功能码掩码:用于从异常响应中提取原始功能码 + * 例如:异常 FC=0x83,原始 FC = 0x83 & 0x7F = 0x03 + */ + public static final int FC_MASK = 0x7F; + + // ==================== 功能码分类判断 ==================== + + /** + * 判断是否为读响应(FC01-04) + */ + public static boolean isReadResponse(int functionCode) { + return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS; + } + + /** + * 判断是否为写响应(FC05/06/15/16) + */ + public static boolean isWriteResponse(int functionCode) { + return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER + || functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS; + } + + /** + * 判断是否为异常响应 + */ + public static boolean isExceptionResponse(int functionCode) { + return (functionCode & FC_EXCEPTION_MASK) != 0; + } + + /** + * 从异常响应中提取原始功能码 + */ + public static int extractOriginalFunctionCode(int exceptionFunctionCode) { + return exceptionFunctionCode & FC_MASK; + } + + /** + * 判断读功能码是否支持写操作 + *

+ * FC01(读线圈)和 FC03(读保持寄存器)支持写操作; + * FC02(读离散输入)和 FC04(读输入寄存器)为只读。 + * + * @param readFunctionCode 读功能码(FC01-04) + * @return 是否支持写操作 + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean isWritable(int readFunctionCode) { + return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS; + } + + /** + * 获取单写功能码 + *

+ * FC01(读线圈)→ FC05(写单个线圈); + * FC03(读保持寄存器)→ FC06(写单个寄存器); + * 其他返回 null(不支持写)。 + * + * @param readFunctionCode 读功能码 + * @return 单写功能码,不支持写时返回 null + */ + @SuppressWarnings("EnhancedSwitchMigration") + public static Integer getWriteSingleFunctionCode(int readFunctionCode) { + switch (readFunctionCode) { + case FC_READ_COILS: + return FC_WRITE_SINGLE_COIL; + case FC_READ_HOLDING_REGISTERS: + return FC_WRITE_SINGLE_REGISTER; + default: + return null; + } + } + + /** + * 获取多写功能码 + *

+ * FC01(读线圈)→ FC15(写多个线圈); + * FC03(读保持寄存器)→ FC16(写多个寄存器); + * 其他返回 null(不支持写)。 + * + * @param readFunctionCode 读功能码 + * @return 多写功能码,不支持写时返回 null + */ + @SuppressWarnings("EnhancedSwitchMigration") + public static Integer getWriteMultipleFunctionCode(int readFunctionCode) { + switch (readFunctionCode) { + case FC_READ_COILS: + return FC_WRITE_MULTIPLE_COILS; + case FC_READ_HOLDING_REGISTERS: + return FC_WRITE_MULTIPLE_REGISTERS; + default: + return null; + } + } + + // ==================== CRC16 工具 ==================== + + /** + * 计算 CRC-16/MODBUS + * + * @param data 数据 + * @param length 计算长度 + * @return CRC16 值 + */ + public static int calculateCrc16(byte[] data, int length) { + int crc = 0xFFFF; + for (int i = 0; i < length; i++) { + crc ^= (data[i] & 0xFF); + for (int j = 0; j < 8; j++) { + if ((crc & 0x0001) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; + } + + /** + * 校验 CRC16 + * + * @param data 包含 CRC 的完整数据 + * @return 校验是否通过 + */ + public static boolean verifyCrc16(byte[] data) { + if (data.length < 3) { + return false; + } + int computed = calculateCrc16(data, data.length - 2); + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); + return computed == received; + } + + // ==================== 数据转换 ==================== + + /** + * 将原始值转换为物模型属性值 + * + * @param rawValues 原始值数组(寄存器值或线圈值) + * @param point 点位配置 + * @return 转换后的属性值 + */ + public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { + if (ArrayUtil.isEmpty(rawValues)) { + return null; + } + String rawDataType = point.getRawDataType(); + String byteOrder = point.getByteOrder(); + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); + + // 1. 根据原始数据类型解析原始数值 + Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); + if (rawNumber == null) { + return null; + } + + // 2. 应用缩放因子:实际值 = 原始值 × scale + BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); + + // 3. 根据数据类型返回合适的 Java 类型 + return formatValue(actualValue, rawDataType); + } + + /** + * 将物模型属性值转换为原始寄存器值 + * + * @param propertyValue 属性值 + * @param point 点位配置 + * @return 原始值数组 + */ + public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { + if (propertyValue == null) { + return new int[0]; + } + String rawDataType = point.getRawDataType(); + String byteOrder = point.getByteOrder(); + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); + int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1); + + // 1. 转换为 BigDecimal + BigDecimal actualValue = new BigDecimal(propertyValue.toString()); + + // 2. 应用缩放因子:原始值 = 实际值 ÷ scale + BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); + + // 3. 根据原始数据类型编码为寄存器值 + return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); + } + + @SuppressWarnings("EnhancedSwitchMigration") + private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); + if (dataTypeEnum == null) { + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); + return rawValues[0]; + } + switch (dataTypeEnum) { + case BOOLEAN: + return rawValues[0] != 0 ? 1 : 0; + case INT16: + return (short) rawValues[0]; + case UINT16: + return rawValues[0] & 0xFFFF; + case INT32: + return parseInt32(rawValues, byteOrder); + case UINT32: + return parseUint32(rawValues, byteOrder); + case FLOAT: + return parseFloat(rawValues, byteOrder); + case DOUBLE: + return parseDouble(rawValues, byteOrder); + default: + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); + return rawValues[0]; + } + } + + private static int parseInt32(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); + } + + private static long parseUint32(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return rawValues[0] & 0xFFFFFFFFL; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; + } + + private static float parseFloat(int[] rawValues, String byteOrder) { + if (rawValues.length < 2) { + return (float) rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); + } + + private static double parseDouble(int[] rawValues, String byteOrder) { + if (rawValues.length < 4) { + return rawValues[0]; + } + byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); + } + + private static byte[] registersToBytes(int[] registers, int count) { + byte[] bytes = new byte[count * 2]; + for (int i = 0; i < Math.min(registers.length, count); i++) { + bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); + bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); + } + return bytes; + } + + @SuppressWarnings("EnhancedSwitchMigration") + private static byte[] reorderBytes(byte[] bytes, String byteOrder) { + IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder); + // null 或者大端序,不需要调整 + if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) { + return bytes; + } + + // 其他字节序调整 + byte[] result = new byte[bytes.length]; + switch (byteOrderEnum) { + case BA: // 小端序:按每 2 字节一组交换(16 位场景 [1,0],32 位场景 [1,0,3,2]) + for (int i = 0; i + 1 < bytes.length; i += 2) { + result[i] = bytes[i + 1]; + result[i + 1] = bytes[i]; + } + break; + case CDAB: // 大端字交换(32 位) + if (bytes.length >= 4) { + result[0] = bytes[2]; + result[1] = bytes[3]; + result[2] = bytes[0]; + result[3] = bytes[1]; + } + break; + case DCBA: // 小端序(32 位) + if (bytes.length >= 4) { + result[0] = bytes[3]; + result[1] = bytes[2]; + result[2] = bytes[1]; + result[3] = bytes[0]; + } + break; + case BADC: // 小端字交换(32 位) + if (bytes.length >= 4) { + result[0] = bytes[1]; + result[1] = bytes[0]; + result[2] = bytes[3]; + result[3] = bytes[2]; + } + break; + default: + return bytes; + } + return result; + } + + @SuppressWarnings("EnhancedSwitchMigration") + private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); + if (dataTypeEnum == null) { + return new int[]{rawValue.intValue()}; + } + switch (dataTypeEnum) { + case BOOLEAN: + return new int[]{rawValue.intValue() != 0 ? 1 : 0}; + case INT16: + case UINT16: + return new int[]{rawValue.intValue() & 0xFFFF}; + case INT32: + return encodeInt32(rawValue.intValue(), byteOrder); + case UINT32: + // 使用 longValue() 避免超过 Integer.MAX_VALUE 时溢出, + // 强转 int 保留低 32 位 bit pattern,写入寄存器的字节是正确的无符号值 + return encodeInt32((int) rawValue.longValue(), byteOrder); + case FLOAT: + return encodeFloat(rawValue.floatValue(), byteOrder); + case DOUBLE: + return encodeDouble(rawValue.doubleValue(), byteOrder); + default: + return new int[]{rawValue.intValue()}; + } + } + + private static int[] encodeInt32(int value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private static int[] encodeFloat(float value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private static int[] encodeDouble(double value, String byteOrder) { + byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); + bytes = reorderBytes(bytes, byteOrder); + return bytesToRegisters(bytes); + } + + private static int[] bytesToRegisters(byte[] bytes) { + int[] registers = new int[bytes.length / 2]; + for (int i = 0; i < registers.length; i++) { + registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); + } + return registers; + } + + @SuppressWarnings("EnhancedSwitchMigration") + private static Object formatValue(BigDecimal value, String rawDataType) { + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); + if (dataTypeEnum == null) { + return value; + } + switch (dataTypeEnum) { + case BOOLEAN: + return value.intValue() != 0; + case INT16: + case INT32: + return value.intValue(); + case UINT16: + case UINT32: + return value.longValue(); + case FLOAT: + return value.floatValue(); + case DOUBLE: + return value.doubleValue(); + default: + return value; + } + } + + // ==================== 帧值提取 ==================== + + /** + * 从帧中提取寄存器值(FC01-04 读响应) + * + * @param frame 解码后的 Modbus 帧 + * @return 寄存器值数组(int[]),失败返回 null + */ + @SuppressWarnings("EnhancedSwitchMigration") + public static int[] extractValues(IotModbusFrame frame) { + if (frame == null || frame.isException()) { + return null; + } + byte[] pdu = frame.getPdu(); + if (pdu == null || pdu.length < 1) { + return null; + } + + int functionCode = frame.getFunctionCode(); + switch (functionCode) { + case FC_READ_COILS: + case FC_READ_DISCRETE_INPUTS: + return extractCoilValues(pdu); + case FC_READ_HOLDING_REGISTERS: + case FC_READ_INPUT_REGISTERS: + return extractRegisterValues(pdu); + default: + log.warn("[extractValues][不支持的功能码: {}]", functionCode); + return null; + } + } + + private static int[] extractCoilValues(byte[] pdu) { + if (pdu.length < 2) { + return null; + } + int byteCount = pdu[0] & 0xFF; + int bitCount = byteCount * 8; + int[] values = new int[bitCount]; + for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) { + values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01); + } + return values; + } + + private static int[] extractRegisterValues(byte[] pdu) { + if (pdu.length < 2) { + return null; + } + int byteCount = pdu[0] & 0xFF; + int registerCount = byteCount / 2; + int[] values = new int[registerCount]; + for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) { + values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF); + } + return values; + } + + /** + * 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断) + * + * @param frame 解码后的 Modbus 响应帧 + * @return registerCount,无法提取时返回 -1(匹配时跳过校验) + */ + public static int extractRegisterCountFromResponse(IotModbusFrame frame) { + byte[] pdu = frame.getPdu(); + if (pdu == null || pdu.length < 1) { + return -1; + } + int byteCount = pdu[0] & 0xFF; + int fc = frame.getFunctionCode(); + // FC03/04 寄存器读响应:registerCount = byteCount / 2 + if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) { + return byteCount / 2; + } + // FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验 + return -1; + } + + // ==================== 点位查找 ==================== + + /** + * 查找点位配置 + * + * @param config 设备 Modbus 配置 + * @param identifier 点位标识符 + * @return 匹配的点位配置,未找到返回 null + */ + public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { + if (config == null || StrUtil.isBlank(identifier)) { + return null; + } + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); + } + + /** + * 根据点位 ID 查找点位配置 + * + * @param config 设备 Modbus 配置 + * @param pointId 点位 ID + * @return 匹配的点位配置,未找到返回 null + */ + public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) { + if (config == null || pointId == null) { + return null; + } + return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java new file mode 100644 index 000000000..1324f3aa5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java @@ -0,0 +1,195 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager; +import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; +import com.ghgande.j2mod.modbus.msg.*; +import com.ghgande.j2mod.modbus.procimg.InputRegister; +import com.ghgande.j2mod.modbus.procimg.Register; +import com.ghgande.j2mod.modbus.procimg.SimpleRegister; +import com.ghgande.j2mod.modbus.util.BitVector; +import io.vertx.core.Future; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.*; + +/** + * IoT Modbus TCP 客户端工具类 + *

+ * 封装基于 j2mod 的 Modbus TCP 读写操作: + * 1. 根据功能码创建对应的 Modbus 读/写请求 + * 2. 通过 {@link IotModbusTcpClientConnectionManager.ModbusConnection} 执行事务 + * 3. 从响应中提取原始值 + * + * @author 芋道源码 + */ +@UtilityClass +@Slf4j +public class IotModbusTcpClientUtils { + + /** + * 读取 Modbus 数据 + * + * @param connection Modbus 连接 + * @param slaveId 从站地址 + * @param point 点位配置 + * @return 原始值(int 数组) + */ + public static Future read(IotModbusTcpClientConnectionManager.ModbusConnection connection, + Integer slaveId, + IotModbusPointRespDTO point) { + return connection.executeBlocking(tcpConnection -> { + try { + // 1. 创建请求 + ModbusRequest request = createReadRequest(point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount()); + request.setUnitID(slaveId); + + // 2. 执行事务(请求) + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); + transaction.setRequest(request); + transaction.execute(); + + // 3. 解析响应 + ModbusResponse response = transaction.getResponse(); + return extractValues(response, point.getFunctionCode()); + } catch (Exception e) { + throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, address=%d]", + slaveId, point.getIdentifier(), point.getRegisterAddress()), e); + } + }); + } + + /** + * 写入 Modbus 数据 + * + * @param connection Modbus 连接 + * @param slaveId 从站地址 + * @param point 点位配置 + * @param values 要写入的值 + * @return 是否成功 + */ + public static Future write(IotModbusTcpClientConnectionManager.ModbusConnection connection, + Integer slaveId, + IotModbusPointRespDTO point, + int[] values) { + return connection.executeBlocking(tcpConnection -> { + try { + // 1. 创建请求 + ModbusRequest request = createWriteRequest(point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount(), values); + if (request == null) { + throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作"); + } + request.setUnitID(slaveId); + + // 2. 执行事务(请求) + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection); + transaction.setRequest(request); + transaction.execute(); + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Modbus 写入失败 [slaveId=%d, identifier=%s, address=%d]", + slaveId, point.getIdentifier(), point.getRegisterAddress()), e); + } + }); + } + + /** + * 创建读取请求 + */ + @SuppressWarnings("EnhancedSwitchMigration") + private static ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { + switch (functionCode) { + case FC_READ_COILS: + return new ReadCoilsRequest(address, count); + case FC_READ_DISCRETE_INPUTS: + return new ReadInputDiscretesRequest(address, count); + case FC_READ_HOLDING_REGISTERS: + return new ReadMultipleRegistersRequest(address, count); + case FC_READ_INPUT_REGISTERS: + return new ReadInputRegistersRequest(address, count); + default: + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + } + + /** + * 创建写入请求 + */ + @SuppressWarnings("EnhancedSwitchMigration") + private static ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { + switch (functionCode) { + case FC_READ_COILS: // 写线圈(使用功能码 5 或 15) + if (count == 1) { + return new WriteCoilRequest(address, values[0] != 0); + } else { + BitVector bv = new BitVector(count); + for (int i = 0; i < Math.min(values.length, count); i++) { + bv.setBit(i, values[i] != 0); + } + return new WriteMultipleCoilsRequest(address, bv); + } + case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16) + if (count == 1) { + return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); + } else { + Register[] registers = new SimpleRegister[count]; + for (int i = 0; i < count; i++) { + registers[i] = new SimpleRegister(i < values.length ? values[i] : 0); + } + return new WriteMultipleRegistersRequest(address, registers); + } + case FC_READ_DISCRETE_INPUTS: // 只读 + case FC_READ_INPUT_REGISTERS: // 只读 + return null; + default: + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + } + + /** + * 从响应中提取值 + */ + @SuppressWarnings("EnhancedSwitchMigration") + private static int[] extractValues(ModbusResponse response, Integer functionCode) { + switch (functionCode) { + case FC_READ_COILS: + ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; + int bitCount = coilsResponse.getBitCount(); + int[] coilValues = new int[bitCount]; + for (int i = 0; i < bitCount; i++) { + coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0; + } + return coilValues; + case FC_READ_DISCRETE_INPUTS: + ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response; + int discreteCount = discretesResponse.getBitCount(); + int[] discreteValues = new int[discreteCount]; + for (int i = 0; i < discreteCount; i++) { + discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0; + } + return discreteValues; + case FC_READ_HOLDING_REGISTERS: + ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response; + InputRegister[] holdingRegisters = holdingResponse.getRegisters(); + int[] holdingValues = new int[holdingRegisters.length]; + for (int i = 0; i < holdingRegisters.length; i++) { + holdingValues[i] = holdingRegisters[i].getValue(); + } + return holdingValues; + case FC_READ_INPUT_REGISTERS: + ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response; + InputRegister[] inputRegisters = inputResponse.getRegisters(); + int[] inputValues = new int[inputRegisters.length]; + for (int i = 0; i < inputRegisters.length; i++) { + inputValues[i] = inputRegisters[i].getValue(); + } + return inputValues; + default: + throw new IllegalArgumentException("不支持的功能码: " + functionCode); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java new file mode 100644 index 000000000..174314095 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT Modbus TCP Client 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotModbusTcpClientConfig { + + /** + * 配置刷新间隔(秒) + */ + @NotNull(message = "配置刷新间隔不能为空") + @Min(value = 1, message = "配置刷新间隔不能小于 1 秒") + private Integer configRefreshInterval = 30; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java new file mode 100644 index 000000000..f632b62fe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java @@ -0,0 +1,218 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * IoT 网关 Modbus TCP Client 协议:主动轮询 Modbus 从站设备数据 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpClientProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private final Vertx vertx; + /** + * 配置刷新定时器 ID + */ + private Long configRefreshTimerId; + + /** + * 连接管理器 + */ + private final IotModbusTcpClientConnectionManager connectionManager; + /** + * 下行消息订阅者 + */ + private IotModbusTcpClientDownstreamSubscriber downstreamSubscriber; + + private final IotModbusTcpClientConfigCacheService configCacheService; + private final IotModbusTcpClientPollScheduler pollScheduler; + + public IotModbusTcpClientProtocol(ProtocolProperties properties) { + IotModbusTcpClientConfig modbusTcpClientConfig = properties.getModbusTcpClient(); + Assert.notNull(modbusTcpClientConfig, "Modbus TCP Client 协议配置(modbusTcpClient)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化 Vertx + this.vertx = Vertx.vertx(); + + // 初始化 Manager + RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class); + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.configCacheService = new IotModbusTcpClientConfigCacheService(deviceApi); + this.connectionManager = new IotModbusTcpClientConnectionManager(redissonClient, vertx, + messageService, configCacheService, serverId); + + // 初始化 Handler + IotModbusTcpClientUpstreamHandler upstreamHandler = new IotModbusTcpClientUpstreamHandler(messageService, serverId); + + // 初始化轮询调度器 + this.pollScheduler = new IotModbusTcpClientPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.MODBUS_TCP_CLIENT; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT Modbus TCP Client 协议 {} 已经在运行中]", getId()); + return; + } + + try { + // 1.1 首次加载配置 + refreshConfig(); + // 1.2 启动配置刷新定时器 + int refreshInterval = properties.getModbusTcpClient().getConfigRefreshInterval(); + configRefreshTimerId = vertx.setPeriodic( + TimeUnit.SECONDS.toMillis(refreshInterval), + id -> refreshConfig() + ); + running = true; + log.info("[start][IoT Modbus TCP Client 协议 {} 启动成功,serverId={}]", getId(), serverId); + + // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotModbusTcpClientDownstreamHandler downstreamHandler = new IotModbusTcpClientDownstreamHandler(connectionManager, + configCacheService); + this.downstreamSubscriber = new IotModbusTcpClientDownstreamSubscriber(this, downstreamHandler, messageBus); + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT Modbus TCP Client 协议 {} 启动失败]", getId(), e); + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 取消配置刷新定时器 + if (configRefreshTimerId != null) { + vertx.cancelTimer(configRefreshTimerId); + configRefreshTimerId = null; + } + // 2.2 停止轮询调度器 + pollScheduler.stopAll(); + // 2.3 关闭所有连接 + connectionManager.closeAll(); + + // 3. 关闭 Vert.x 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT Modbus TCP Client 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT Modbus TCP Client 协议 {} Vertx 关闭失败]", getId(), e); + } + } + running = false; + log.info("[stop][IoT Modbus TCP Client 协议 {} 已停止]", getId()); + } + + /** + * 刷新配置 + */ + private synchronized void refreshConfig() { + try { + // 1. 从 biz 拉取最新配置(API 失败时返回 null) + List configs = configCacheService.refreshConfig(); + if (configs == null) { + log.warn("[refreshConfig][API 失败,跳过本轮刷新]"); + return; + } + log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); + + // 2. 更新连接和轮询任务 + for (IotModbusDeviceConfigRespDTO config : configs) { + try { + // 2.1 确保连接存在 + connectionManager.ensureConnection(config); + // 2.2 更新轮询任务 + pollScheduler.updatePolling(config); + } catch (Exception e) { + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); + } + } + + // 3. 清理已删除设备的资源 + Set removedDeviceIds = configCacheService.cleanupRemovedDevices(configs); + for (Long deviceId : removedDeviceIds) { + pollScheduler.stopPolling(deviceId); + connectionManager.removeDevice(deviceId); + } + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java new file mode 100644 index 000000000..045e61fdf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * IoT Modbus TCP Client 下行消息处理器 + *

+ * 负责: + * 1. 处理下行消息(如属性设置 thing.service.property.set) + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpClientDownstreamHandler { + + private final IotModbusTcpClientConnectionManager connectionManager; + private final IotModbusTcpClientConfigCacheService configCacheService; + + /** + * 处理下行消息 + */ + @SuppressWarnings({"unchecked", "DuplicatedCode"}) + public void handle(IotDeviceMessage message) { + // 1.1 检查是否是属性设置消息 + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) { + return; + } + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { + log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod()); + return; + } + // 1.2 获取设备配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId()); + if (config == null) { + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId()); + return; + } + + // 2. 解析属性值并写入 + Object params = message.getParams(); + if (!(params instanceof Map)) { + log.warn("[handle][params 不是 Map 类型: {}]", params); + return; + } + Map propertyMap = (Map) params; + for (Map.Entry entry : propertyMap.entrySet()) { + String identifier = entry.getKey(); + Object value = entry.getValue(); + // 2.1 查找对应的点位配置 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier); + if (point == null) { + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); + continue; + } + // 2.2 检查是否支持写操作 + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) { + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); + continue; + } + + // 2.3 执行写入 + writeProperty(config, point, value); + } + } + + /** + * 写入属性值 + */ + private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) { + // 1.1 获取连接 + IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); + if (connection == null) { + log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId()); + return; + } + // 1.2 获取 slave ID + Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); + if (slaveId == null) { + log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId()); + return; + } + + // 2.1 转换属性值为原始值 + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point); + // 2.2 执行 Modbus 写入 + IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues) + .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", + config.getDeviceId(), point.getIdentifier(), value)) + .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java new file mode 100644 index 000000000..6c8a4be9b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpClientDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { + + private final IotModbusTcpClientDownstreamHandler downstreamHandler; + + public IotModbusTcpClientDownstreamSubscriber(IotModbusTcpClientProtocol protocol, + IotModbusTcpClientDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java new file mode 100644 index 000000000..2b7e9c206 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * IoT Modbus TCP 上行数据处理器:将原始值转换为物模型属性值并上报 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpClientUpstreamHandler { + + private final IotDeviceMessageService messageService; + + private final String serverId; + + public IotModbusTcpClientUpstreamHandler(IotDeviceMessageService messageService, + String serverId) { + this.messageService = messageService; + this.serverId = serverId; + } + + /** + * 处理 Modbus 读取结果 + * + * @param config 设备配置 + * @param point 点位配置 + * @param rawValue 原始值(int 数组) + */ + public void handleReadResult(IotModbusDeviceConfigRespDTO config, + IotModbusPointRespDTO point, + int[] rawValue) { + try { + // 1.1 转换原始值为物模型属性值(点位翻译) + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point); + log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", + config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); + // 1.2 构造属性上报消息 + Map params = MapUtil.of(point.getIdentifier(), convertedValue); + IotDeviceMessage message = IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); + + // 2. 发送到消息总线 + messageService.sendDeviceMessage(message, config.getProductKey(), + config.getDeviceName(), serverId); + } catch (Exception e) { + log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]", + config.getDeviceId(), point.getIdentifier(), e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java new file mode 100644 index 000000000..fa08207df --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * IoT Modbus TCP Client 配置缓存服务 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpClientConfigCacheService { + + private final IotDeviceCommonApi deviceApi; + + /** + * 配置缓存:deviceId -> 配置 + */ + private final Map configCache = new ConcurrentHashMap<>(); + + /** + * 已知的设备 ID 集合(作用:用于检测已删除的设备) + * + * @see #cleanupRemovedDevices(List) + */ + private final Set knownDeviceIds = ConcurrentHashMap.newKeySet(); + + /** + * 刷新配置 + * + * @return 最新的配置列表;API 失败时返回 null(调用方应跳过 cleanup) + */ + public List refreshConfig() { + try { + // 1. 从远程获取配置 + CommonResult> result = deviceApi.getModbusDeviceConfigList( + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_CLIENT.getType())); + result.checkError(); + List configs = result.getData(); + + // 2. 更新缓存(注意:不在这里更新 knownDeviceIds,由 cleanupRemovedDevices 统一管理) + for (IotModbusDeviceConfigRespDTO config : configs) { + configCache.put(config.getDeviceId(), config); + } + return configs; + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + return null; + } + } + + /** + * 获取设备配置 + * + * @param deviceId 设备 ID + * @return 配置 + */ + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { + return configCache.get(deviceId); + } + + /** + * 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合 + * + * @param currentConfigs 当前有效的配置列表 + * @return 已删除的设备 ID 集合 + */ + public Set cleanupRemovedDevices(List currentConfigs) { + // 1.1 获取当前有效的设备 ID + Set currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId); + // 1.2 找出已删除的设备(基于旧的 knownDeviceIds) + Set removedDeviceIds = new HashSet<>(knownDeviceIds); + removedDeviceIds.removeAll(currentDeviceIds); + + // 2. 清理已删除设备的缓存 + for (Long deviceId : removedDeviceIds) { + log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); + configCache.remove(deviceId); + } + + // 3. 更新已知设备 ID 集合为当前有效的设备 ID + knownDeviceIds.clear(); + knownDeviceIds.addAll(currentDeviceIds); + return removedDeviceIds; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java new file mode 100644 index 000000000..bfdacd020 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java @@ -0,0 +1,317 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import com.ghgande.j2mod.modbus.net.TCPMasterConnection; +import io.vertx.core.Context; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT Modbus TCP 连接管理器 + *

+ * 统一管理 Modbus TCP 连接: + * 1. 管理 TCP 连接(相同 ip:port 共用连接) + * 2. 分布式锁管理(连接级别),避免多节点重复创建连接 + * 3. 连接重试和故障恢复 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpClientConnectionManager { + + private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:"; + + private final RedissonClient redissonClient; + private final Vertx vertx; + private final IotDeviceMessageService messageService; + private final IotModbusTcpClientConfigCacheService configCacheService; + private final String serverId; + + /** + * 连接池:key = ip:port + */ + private final Map connectionPool = new ConcurrentHashMap<>(); + + /** + * 设备 ID 到连接 key 的映射 + */ + private final Map deviceConnectionMap = new ConcurrentHashMap<>(); + + public IotModbusTcpClientConnectionManager(RedissonClient redissonClient, Vertx vertx, + IotDeviceMessageService messageService, + IotModbusTcpClientConfigCacheService configCacheService, + String serverId) { + this.redissonClient = redissonClient; + this.vertx = vertx; + this.messageService = messageService; + this.configCacheService = configCacheService; + this.serverId = serverId; + } + + /** + * 确保连接存在 + *

+ * 首次建连成功时,直接发送设备上线消息 + * + * @param config 设备配置 + */ + public void ensureConnection(IotModbusDeviceConfigRespDTO config) { + // 1.1 检查设备是否切换了 IP/端口,若是则先清理旧连接 + String connectionKey = buildConnectionKey(config.getIp(), config.getPort()); + String oldConnectionKey = deviceConnectionMap.get(config.getDeviceId()); + if (oldConnectionKey != null && ObjUtil.notEqual(oldConnectionKey, connectionKey)) { + log.info("[ensureConnection][设备 {} IP/端口变更: {} -> {}, 清理旧连接]", + config.getDeviceId(), oldConnectionKey, connectionKey); + removeDevice(config.getDeviceId()); + } + // 1.2 记录设备与连接的映射 + deviceConnectionMap.put(config.getDeviceId(), connectionKey); + + // 2. 情况一:连接已存在,注册设备并发送上线消息 + ModbusConnection connection = connectionPool.get(connectionKey); + if (connection != null) { + addDeviceAndOnline(connection, config); + return; + } + + // 3. 情况二:连接不存在,加分布式锁创建新连接 + RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey); + if (!lock.tryLock()) { + log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey); + return; + } + try { + // 3.1 double-check:拿到锁后再次检查,避免并发创建重复连接 + connection = connectionPool.get(connectionKey); + if (connection != null) { + addDeviceAndOnline(connection, config); + lock.unlock(); + return; + } + // 3.2 创建新连接 + connection = createConnection(config); + connection.setLock(lock); + connectionPool.put(connectionKey, connection); + log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); + // 3.3 注册设备并发送上线消息 + addDeviceAndOnline(connection, config); + } catch (Exception e) { + log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e); + // 建连失败,释放锁让其他节点可重试 + lock.unlock(); + } + } + + /** + * 创建 Modbus TCP 连接 + */ + private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config) throws Exception { + // 1. 创建 TCP 连接 + TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp())); + tcpConnection.setPort(config.getPort()); + tcpConnection.setTimeout(config.getTimeout()); + tcpConnection.connect(); + + // 2. 创建 Modbus 连接对象 + return new ModbusConnection() + .setConnectionKey(buildConnectionKey(config.getIp(), config.getPort())) + .setTcpConnection(tcpConnection).setContext(vertx.getOrCreateContext()) + .setTimeout(config.getTimeout()).setRetryInterval(config.getRetryInterval()); + } + + /** + * 获取连接 + */ + public ModbusConnection getConnection(Long deviceId) { + String connectionKey = deviceConnectionMap.get(deviceId); + if (connectionKey == null) { + return null; + } + return connectionPool.get(connectionKey); + } + + /** + * 获取设备的 slave ID + */ + public Integer getSlaveId(Long deviceId) { + ModbusConnection connection = getConnection(deviceId); + if (connection == null) { + return null; + } + return connection.getSlaveId(deviceId); + } + + /** + * 移除设备 + *

+ * 移除时直接发送设备下线消息 + */ + public void removeDevice(Long deviceId) { + // 1.1 移除设备时,发送下线消息 + sendOfflineMessage(deviceId); + // 1.2 移除设备引用 + String connectionKey = deviceConnectionMap.remove(deviceId); + if (connectionKey == null) { + return; + } + + // 2.1 移除连接中的设备引用 + ModbusConnection connection = connectionPool.get(connectionKey); + if (connection == null) { + return; + } + connection.removeDevice(deviceId); + // 2.2 如果没有设备引用了,关闭连接 + if (connection.getDeviceCount() == 0) { + closeConnection(connectionKey); + } + } + + // ==================== 设备连接 & 上下线消息 ==================== + + /** + * 注册设备到连接,并发送上线消息 + */ + private void addDeviceAndOnline(ModbusConnection connection, + IotModbusDeviceConfigRespDTO config) { + Integer previous = connection.addDevice(config.getDeviceId(), config.getSlaveId()); + // 首次注册,发送上线消息 + if (previous == null) { + sendOnlineMessage(config); + } + } + + /** + * 发送设备上线消息 + */ + private void sendOnlineMessage(IotModbusDeviceConfigRespDTO config) { + try { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + messageService.sendDeviceMessage(onlineMessage, + config.getProductKey(), config.getDeviceName(), serverId); + } catch (Exception ex) { + log.error("[sendOnlineMessage][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex); + } + } + + /** + * 发送设备下线消息 + */ + private void sendOfflineMessage(Long deviceId) { + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); + if (config == null) { + return; + } + try { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + messageService.sendDeviceMessage(offlineMessage, + config.getProductKey(), config.getDeviceName(), serverId); + } catch (Exception ex) { + log.error("[sendOfflineMessage][发送设备下线消息失败, deviceId={}]", deviceId, ex); + } + } + + /** + * 关闭指定连接 + */ + private void closeConnection(String connectionKey) { + ModbusConnection connection = connectionPool.remove(connectionKey); + if (connection == null) { + return; + } + + try { + if (connection.getTcpConnection() != null) { + connection.getTcpConnection().close(); + } + // 释放分布式锁,让其他节点可接管 + RLock lock = connection.getLock(); + if (lock != null && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); + } catch (Exception e) { + log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e); + } + } + + /** + * 关闭所有连接 + */ + public void closeAll() { + // 先复制再遍历,避免 closeConnection 中 remove 导致并发修改 + List connectionKeys = new ArrayList<>(connectionPool.keySet()); + for (String connectionKey : connectionKeys) { + closeConnection(connectionKey); + } + deviceConnectionMap.clear(); + } + + private String buildConnectionKey(String ip, Integer port) { + return ip + ":" + port; + } + + /** + * Modbus 连接信息 + */ + @Data + public static class ModbusConnection { + + private String connectionKey; + private TCPMasterConnection tcpConnection; + private Integer timeout; + private Integer retryInterval; + /** + * 设备 ID 到 slave ID 的映射 + */ + private final Map deviceSlaveMap = new ConcurrentHashMap<>(); + + /** + * 分布式锁,锁住连接的创建和销毁,避免多节点重复连接同一从站 + */ + private RLock lock; + + /** + * Vert.x Context,用于 executeBlocking 执行 Modbus 操作,保证同一连接的操作串行执行 + */ + private Context context; + + public Integer addDevice(Long deviceId, Integer slaveId) { + return deviceSlaveMap.putIfAbsent(deviceId, slaveId); + } + + public void removeDevice(Long deviceId) { + deviceSlaveMap.remove(deviceId); + } + + public int getDeviceCount() { + return deviceSlaveMap.size(); + } + + public Integer getSlaveId(Long deviceId) { + return deviceSlaveMap.get(deviceId); + } + + /** + * 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行) + */ + public Future executeBlocking(java.util.function.Function operation) { + // ordered=true 保证同一 Context 的操作串行执行,不同连接之间可并行 + return context.executeBlocking(() -> operation.apply(tcpConnection), true); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java new file mode 100644 index 000000000..946937d40 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler; +import io.vertx.core.Vertx; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT Modbus TCP Client 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollScheduler { + + private final IotModbusTcpClientConnectionManager connectionManager; + private final IotModbusTcpClientUpstreamHandler upstreamHandler; + private final IotModbusTcpClientConfigCacheService configCacheService; + + public IotModbusTcpClientPollScheduler(Vertx vertx, + IotModbusTcpClientConnectionManager connectionManager, + IotModbusTcpClientUpstreamHandler upstreamHandler, + IotModbusTcpClientConfigCacheService configCacheService) { + super(vertx); + this.connectionManager = connectionManager; + this.upstreamHandler = upstreamHandler; + this.configCacheService = configCacheService; + } + + // ========== 轮询执行 ========== + + /** + * 轮询单个点位 + */ + @Override + protected void pollPoint(Long deviceId, Long pointId) { + // 1.1 从 configCache 获取最新配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); + if (config == null || CollUtil.isEmpty(config.getPoints())) { + log.warn("[pollPoint][设备 {} 没有配置]", deviceId); + return; + } + // 1.2 查找点位 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId); + if (point == null) { + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); + return; + } + + // 2.1 获取连接 + IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId); + if (connection == null) { + log.warn("[pollPoint][设备 {} 没有连接]", deviceId); + return; + } + // 2.2 获取 slave ID + Integer slaveId = connectionManager.getSlaveId(deviceId); + Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId); + + // 3. 执行 Modbus 读取 + IotModbusTcpClientUtils.read(connection, slaveId, point) + .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) + .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", + deviceId, point.getIdentifier(), e)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java new file mode 100644 index 000000000..3bd4fa95f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java @@ -0,0 +1,6 @@ +/** + * Modbus TCP Client(主站)协议:网关主动连接并轮询 Modbus 从站设备 + *

+ * 基于 j2mod 实现,支持 FC01-04 读、FC05/06/15/16 写,定时轮询 + 下发属性设置 + */ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java new file mode 100644 index 000000000..5b4fd2236 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT Modbus TCP Server 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotModbusTcpServerConfig { + + /** + * 配置刷新间隔(秒) + */ + @NotNull(message = "配置刷新间隔不能为空") + @Min(value = 1, message = "配置刷新间隔不能小于 1 秒") + private Integer configRefreshInterval = 30; + + /** + * 自定义功能码(用于认证等扩展交互) + * Modbus 协议保留 65-72 给用户自定义,默认 65 + */ + @NotNull(message = "自定义功能码不能为空") + @Min(value = 65, message = "自定义功能码不能小于 65") + @Max(value = 72, message = "自定义功能码不能大于 72") + private Integer customFunctionCode = 65; + + /** + * Pending Request 超时时间(毫秒) + */ + @NotNull(message = "请求超时时间不能为空") + private Integer requestTimeout = 5000; + + /** + * Pending Request 清理间隔(毫秒) + */ + @NotNull(message = "请求清理间隔不能为空") + private Integer requestCleanupInterval = 10000; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java new file mode 100644 index 000000000..80ce9eec0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java @@ -0,0 +1,334 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream.IotModbusTcpServerUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +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.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * IoT 网关 Modbus TCP Server 协议 + *

+ * 作为 TCP Server 接收设备主动连接: + * 1. 设备通过自定义功能码(FC 65)发送认证请求 + * 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式) + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpServerProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private final Vertx vertx; + /** + * TCP Server + */ + private NetServer netServer; + /** + * 配置刷新定时器 ID + */ + private Long configRefreshTimerId; + /** + * Pending Request 清理定时器 ID + */ + private Long requestCleanupTimerId; + + /** + * 连接管理器 + */ + private final IotModbusTcpServerConnectionManager connectionManager; + /** + * 下行消息订阅者 + */ + private IotModbusTcpServerDownstreamSubscriber downstreamSubscriber; + + private final IotModbusFrameDecoder frameDecoder; + @SuppressWarnings("FieldCanBeLocal") + private final IotModbusFrameEncoder frameEncoder; + + private final IotModbusTcpServerConfigCacheService configCacheService; + private final IotModbusTcpServerPendingRequestManager pendingRequestManager; + private final IotModbusTcpServerUpstreamHandler upstreamHandler; + private final IotModbusTcpServerPollScheduler pollScheduler; + private final IotDeviceMessageService messageService; + + public IotModbusTcpServerProtocol(ProtocolProperties properties) { + IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer(); + Assert.notNull(slaveConfig, "Modbus TCP Server 协议配置(modbusTcpServer)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化 Vertx + this.vertx = Vertx.vertx(); + + // 初始化 Manager + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.connectionManager = new IotModbusTcpServerConnectionManager(); + this.configCacheService = new IotModbusTcpServerConfigCacheService(deviceApi); + this.pendingRequestManager = new IotModbusTcpServerPendingRequestManager(); + + // 初始化帧编解码器 + this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode()); + this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode()); + + // 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突) + AtomicInteger transactionIdCounter = new AtomicInteger(0); + // 初始化轮询调度器 + this.pollScheduler = new IotModbusTcpServerPollScheduler( + vertx, connectionManager, frameEncoder, pendingRequestManager, + slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService); + + // 初始化 Handler + this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); + this.upstreamHandler = new IotModbusTcpServerUpstreamHandler( + deviceApi, this.messageService, frameEncoder, + connectionManager, configCacheService, pendingRequestManager, + pollScheduler, deviceService, serverId); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.MODBUS_TCP_SERVER; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT Modbus TCP Server 协议 {} 已经在运行中]", getId()); + return; + } + + try { + // 1. 启动配置刷新定时器 + IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer(); + configRefreshTimerId = vertx.setPeriodic( + TimeUnit.SECONDS.toMillis(slaveConfig.getConfigRefreshInterval()), + id -> refreshConfig()); + + // 2.1 启动 TCP Server + startTcpServer(); + + // 2.2 启动 PendingRequest 清理定时器 + requestCleanupTimerId = vertx.setPeriodic( + slaveConfig.getRequestCleanupInterval(), + id -> pendingRequestManager.cleanupExpired()); + running = true; + log.info("[start][IoT Modbus TCP Server 协议 {} 启动成功, serverId={}, port={}]", + getId(), serverId, properties.getPort()); + + // 3. 启动下行消息订阅 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotModbusTcpServerDownstreamHandler downstreamHandler = new IotModbusTcpServerDownstreamHandler( + connectionManager, configCacheService, frameEncoder, this.pollScheduler.getTransactionIdCounter()); + this.downstreamSubscriber = new IotModbusTcpServerDownstreamSubscriber( + this, downstreamHandler, messageBus); + downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT Modbus TCP Server 协议 {} 启动失败]", getId(), e); + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + } catch (Exception e) { + log.error("[stop][下行消息订阅器停止失败]", e); + } + downstreamSubscriber = null; + } + + // 2.1 取消定时器 + if (configRefreshTimerId != null) { + vertx.cancelTimer(configRefreshTimerId); + configRefreshTimerId = null; + } + if (requestCleanupTimerId != null) { + vertx.cancelTimer(requestCleanupTimerId); + requestCleanupTimerId = null; + } + // 2.2 停止轮询 + pollScheduler.stopAll(); + // 2.3 清理 PendingRequest + pendingRequestManager.clear(); + // 2.4 关闭所有连接 + connectionManager.closeAll(); + // 2.5 关闭 TCP Server + if (netServer != null) { + try { + netServer.close().result(); + log.info("[stop][TCP Server 已关闭]"); + } catch (Exception e) { + log.error("[stop][TCP Server 关闭失败]", e); + } + netServer = null; + } + + // 3. 关闭 Vertx + if (vertx != null) { + try { + vertx.close().result(); + } catch (Exception e) { + log.error("[stop][Vertx 关闭失败]", e); + } + } + running = false; + log.info("[stop][IoT Modbus TCP Server 协议 {} 已停止]", getId()); + } + + /** + * 启动 TCP Server + */ + private void startTcpServer() { + // 1. 创建 TCP Server + NetServerOptions options = new NetServerOptions() + .setPort(properties.getPort()); + netServer = vertx.createNetServer(options); + + // 2. 设置连接处理器 + netServer.connectHandler(this::handleConnection); + try { + netServer.listen().toCompletionStage().toCompletableFuture().get(); + log.info("[startTcpServer][TCP Server 启动成功, port={}]", properties.getPort()); + } catch (Exception e) { + throw new RuntimeException("[startTcpServer][TCP Server 启动失败]", e); + } + } + + /** + * 处理新连接 + */ + private void handleConnection(NetSocket socket) { + log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress()); + + // 1. 创建 RecordParser 并设置为数据处理器 + RecordParser recordParser = frameDecoder.createRecordParser((frame, frameFormat) -> { + // 【重要】帧处理分发,即消息处理 + upstreamHandler.handleFrame(socket, frame, frameFormat); + }); + socket.handler(recordParser); + + // 2.1 连接关闭处理 + socket.closeHandler(v -> { + ConnectionInfo info = connectionManager.removeConnection(socket); + if (info == null || info.getDeviceId() == null) { + log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress()); + return; + } + pollScheduler.stopPolling(info.getDeviceId()); + pendingRequestManager.removeDevice(info.getDeviceId()); + configCacheService.removeConfig(info.getDeviceId()); + // 发送设备下线消息 + try { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + messageService.sendDeviceMessage(offlineMessage, info.getProductKey(), info.getDeviceName(), serverId); + } catch (Exception ex) { + log.error("[handleConnection][发送设备下线消息失败, deviceId={}]", info.getDeviceId(), ex); + } + log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", + info.getDeviceId(), socket.remoteAddress()); + }); + // 2.2 异常处理 + socket.exceptionHandler(e -> { + log.error("[handleConnection][连接异常, remoteAddress={}]", socket.remoteAddress(), e); + socket.close(); + }); + } + + /** + * 刷新已连接设备的配置(定时调用) + */ + private synchronized void refreshConfig() { + try { + // 1. 只刷新已连接设备的配置 + Set connectedDeviceIds = connectionManager.getConnectedDeviceIds(); + if (CollUtil.isEmpty(connectedDeviceIds)) { + return; + } + List configs = + configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds); + if (configs == null) { + log.warn("[refreshConfig][刷新配置失败,跳过本次刷新]"); + return; + } + log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size()); + + // 2. 更新已连接设备的轮询任务 + for (IotModbusDeviceConfigRespDTO config : configs) { + try { + pollScheduler.updatePolling(config); + } catch (Exception e) { + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); + } + } + } catch (Exception e) { + log.error("[refreshConfig][刷新配置失败]", e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java new file mode 100644 index 000000000..7aeca9918 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec; + +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * IoT Modbus 统一帧数据模型(TCP/RTU 公用) + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class IotModbusFrame { + + /** + * 从站地址 + */ + private int slaveId; + /** + * 功能码 + */ + private int functionCode; + /** + * PDU 数据(不含 slaveId) + */ + private byte[] pdu; + /** + * 事务标识符 + *

+ * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值 + */ + private Integer transactionId; + + /** + * 异常码 + *

+ * 当功能码最高位为 1 时(异常响应),此字段存储异常码。 + * + * @see IotModbusCommonUtils#FC_EXCEPTION_MASK + */ + private Integer exceptionCode; + + /** + * 自定义功能码时的 JSON 字符串(用于 auth 认证等等) + */ + private String customData; + + /** + * 是否异常响应(基于 exceptionCode 是否有值判断) + */ + public boolean isException() { + return exceptionCode != null; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java new file mode 100644 index 000000000..5b92c3ea7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java @@ -0,0 +1,477 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec; + +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.function.BiConsumer; + +/** + * IoT Modbus 帧解码器:集成 TCP 拆包 + 帧格式探测 + 帧解码,一条龙完成从 TCP 字节流到 IotModbusFrame 的转换。 + *

+ * 流程: + * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU + * 2. 检测后切换到对应的拆包 Handler,并将首包 6 字节通过 handleFirstBytes() 交给新 Handler 处理 + * 3. 拆包完成后解码为 IotModbusFrame,通过回调返回 + * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动) + * - MODBUS_RTU:功能码驱动的状态机 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotModbusFrameDecoder { + + private static final Boolean REQUEST_MODE_DEFAULT = false; + + /** + * 自定义功能码 + */ + private final int customFunctionCode; + + /** + * 创建带自动帧格式检测的 RecordParser(默认响应模式) + * + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式) + * @return RecordParser 实例 + */ + public RecordParser createRecordParser(BiConsumer frameHandler) { + return createRecordParser(frameHandler, REQUEST_MODE_DEFAULT); + } + + /** + * 创建带自动帧格式检测的 RecordParser + * + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式) + * @param requestMode 是否为请求模式(true:接收方收到的是 Modbus 请求帧,FC01-04 按固定 8 字节解析; + * false:接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析) + * @return RecordParser 实例 + */ + public RecordParser createRecordParser(BiConsumer frameHandler, + boolean requestMode) { + // 先创建一个 RecordParser:使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测 + RecordParser parser = RecordParser.newFixed(6); + parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, requestMode)); + return parser; + } + + // ==================== 帧解码 ==================== + + /** + * 解码响应帧(拆包后的完整帧 byte[]) + * + * @param data 完整帧字节数组 + * @param format 帧格式 + * @return 解码后的 IotModbusFrame + */ + private IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) { + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { + return decodeTcpResponse(data); + } else { + return decodeRtuResponse(data); + } + } + + /** + * 解码 MODBUS_TCP 响应 + * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...] + */ + private IotModbusFrame decodeTcpResponse(byte[] data) { + if (data.length < 8) { + log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length); + return null; + } + ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); + int transactionId = buf.getShort() & 0xFFFF; + buf.getShort(); // protocolId:固定 0x0000,Modbus 协议标识 + buf.getShort(); // length:后续字节数(UnitId + PDU),拆包阶段已使用 + int slaveId = buf.get() & 0xFF; + int functionCode = buf.get() & 0xFF; + // 提取 PDU 数据(从 functionCode 之后到末尾) + byte[] pdu = new byte[data.length - 8]; + System.arraycopy(data, 8, pdu, 0, pdu.length); + // 构建 IotModbusFrame + return buildFrame(slaveId, functionCode, pdu, transactionId); + } + + /** + * 解码 MODBUS_RTU 响应 + * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)] + */ + private IotModbusFrame decodeRtuResponse(byte[] data) { + if (data.length < 4) { + log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length); + return null; + } + // 校验 CRC + if (!IotModbusCommonUtils.verifyCrc16(data)) { + log.warn("[decodeRtuResponse][CRC 校验失败]"); + return null; + } + int slaveId = data[0] & 0xFF; + int functionCode = data[1] & 0xFF; + // PDU 数据(不含 slaveId、functionCode、CRC) + byte[] pdu = new byte[data.length - 4]; + System.arraycopy(data, 2, pdu, 0, pdu.length); + // 构建 IotModbusFrame + return buildFrame(slaveId, functionCode, pdu, null); + } + + /** + * 构建 IotModbusFrame + */ + private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) { + IotModbusFrame frame = new IotModbusFrame() + .setSlaveId(slaveId) + .setFunctionCode(functionCode) + .setPdu(pdu) + .setTransactionId(transactionId); + // 异常响应 + if (IotModbusCommonUtils.isExceptionResponse(functionCode)) { + frame.setFunctionCode(IotModbusCommonUtils.extractOriginalFunctionCode(functionCode)); + if (pdu.length >= 1) { + frame.setExceptionCode(pdu[0] & 0xFF); + } + return frame; + } + // 自定义功能码 + if (functionCode == customFunctionCode) { + // data 区格式:[byteCount(1)] [JSON data(N)] + if (pdu.length >= 1) { + int byteCount = pdu[0] & 0xFF; + if (pdu.length >= 1 + byteCount) { + frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8)); + } + } + } + return frame; + } + + // ==================== 拆包 Handler ==================== + + /** + * 帧格式检测阶段 Handler(仅处理首包,探测后切换到对应的拆包 Handler) + */ + @RequiredArgsConstructor + private class DetectPhaseHandler implements Handler { + + private final RecordParser parser; + private final int customFunctionCode; + private final BiConsumer frameHandler; + private final boolean requestMode; + + @Override + public void handle(Buffer buffer) { + // 检测帧格式:protocolId==0x0000 且 length 合法 → MODBUS_TCP,否则 → MODBUS_RTU + byte[] bytes = buffer.getBytes(); + int protocolId = ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); + + // 分别处理 MODBUS_TCP、MODBUS_RTU 两种情况 + if (protocolId == 0x0000 && length >= 1 && length <= 253) { + // MODBUS_TCP:切换到 TCP 拆包 Handler + log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]"); + TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler); + parser.handler(tcpHandler); + // 当前 bytes 就是 MBAP 的前 6 字节,直接交给 tcpHandler 处理 + tcpHandler.handleFirstBytes(bytes); + } else { + // MODBUS_RTU:切换到 RTU 拆包 Handler + log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]"); + RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode, requestMode); + parser.handler(rtuHandler); + // 当前 bytes 包含前 6 字节(slaveId + FC + 部分数据),交给 rtuHandler 处理 + rtuHandler.handleFirstBytes(bytes); + } + } + } + + /** + * MODBUS_TCP 拆包 Handler(两阶段 RecordParser) + *

+ * Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length + * Phase 2: fixedSizeMode(length) → 读 unitId + PDU + */ + @RequiredArgsConstructor + private class TcpFrameHandler implements Handler { + + private final RecordParser parser; + private final BiConsumer frameHandler; + + private byte[] mbapHeader; + private boolean waitingForBody = false; + + /** + * 处理探测阶段传来的首帧 6 字节(即 MBAP 头) + * + * @param bytes 探测阶段消费的 6 字节 + */ + void handleFirstBytes(byte[] bytes) { + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); + this.mbapHeader = bytes; + this.waitingForBody = true; + parser.fixedSizeMode(length); + } + + @Override + public void handle(Buffer buffer) { + if (waitingForBody) { + // Phase 2: 收到 body(unitId + PDU) + byte[] body = buffer.getBytes(); + // 拼接完整帧:MBAP(6) + body + byte[] fullFrame = new byte[mbapHeader.length + body.length]; + System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length); + System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length); + // 解码并回调 + IotModbusFrame frame = decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP); + if (frame != null) { + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_TCP); + } + // 切回 Phase 1 + waitingForBody = false; + mbapHeader = null; + parser.fixedSizeMode(6); + } else { + // Phase 1: 收到 MBAP 头 6 字节 + byte[] header = buffer.getBytes(); + int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF); + if (length < 1 || length > 253) { + log.warn("[TcpFrameHandler][MBAP Length 异常: {}]", length); + parser.fixedSizeMode(6); + return; + } + this.mbapHeader = header; + this.waitingForBody = true; + parser.fixedSizeMode(length); + } + } + } + + /** + * MODBUS_RTU 拆包 Handler(功能码驱动的状态机) + *

+ * 状态机流程: + * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode + * Phase 2: 根据 functionCode 确定剩余长度: + * - 异常响应 (FC & EXCEPTION_MASK):fixedSizeMode(3) → exceptionCode(1) + CRC(2) + * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) + * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) + * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) + *

+ * 请求模式(requestMode=true)时,FC01-04 按固定 8 字节解析(与写响应相同路径), + * 因为读请求格式为 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)] + */ + @RequiredArgsConstructor + private class RtuFrameHandler implements Handler { + + private static final int STATE_HEADER = 0; + private static final int STATE_EXCEPTION_BODY = 1; + private static final int STATE_READ_BYTE_COUNT = 2; + private static final int STATE_READ_DATA = 3; + private static final int STATE_WRITE_BODY = 4; + + private final RecordParser parser; + private final BiConsumer frameHandler; + private final int customFunctionCode; + /** + * 请求模式: + * - true 表示接收方收到的是 Modbus 请求帧(如设备端收到网关下发的读请求),FC01-04 按固定 8 字节帧解析 + * - false 表示接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析 + */ + private final boolean requestMode; + + private int state = STATE_HEADER; + private byte slaveId; + private byte functionCode; + private byte byteCount; + private Buffer pendingData; + private int expectedDataLen; + + /** + * 处理探测阶段传来的首帧 6 字节 + *

+ * 由于 RTU 首帧被探测阶段消费了 6 字节,这里需要从中提取 slaveId + FC 并根据 FC 处理剩余数据 + * + * @param bytes 探测阶段消费的 6 字节:[slaveId][FC][...4 bytes...] + */ + void handleFirstBytes(byte[] bytes) { + this.slaveId = bytes[0]; + this.functionCode = bytes[1]; + int fc = functionCode & 0xFF; + if (IotModbusCommonUtils.isExceptionResponse(fc)) { + // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节 + // 已有 6 字节(多 1 字节),取前 5 字节组装 + Buffer frame = Buffer.buffer(5); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC + emitFrame(frame); + resetToHeader(); + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) { + // 请求模式下的读请求:固定 8 字节 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)] + // 已有 6 字节,还需 2 字节(CRC) + state = STATE_WRITE_BODY; + this.pendingData = Buffer.buffer(); + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节(StartAddr + Quantity) + parser.fixedSizeMode(2); // 还需 2 字节(CRC) + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { + // 读响应或自定义 FC:bytes[2] = byteCount + this.byteCount = bytes[2]; + int bc = byteCount & 0xFF; + // 已有数据:bytes[3..5] = 3 字节 + // 还需:byteCount + CRC(2) - 3 字节已有 + int remaining = bc + 2 - 3; + if (remaining <= 0) { + // 数据已足够,组装完整帧 + int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC + Buffer frame = Buffer.buffer(totalLen); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendByte(byteCount); + frame.appendBytes(bytes, 3, bc + 2); // data + CRC + emitFrame(frame); + resetToHeader(); + } else { + // 需要继续读 + state = STATE_READ_DATA; + this.pendingData = Buffer.buffer(); + this.pendingData.appendBytes(bytes, 3, 3); // 暂存已有的 3 字节 + this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC + parser.fixedSizeMode(remaining); + } + } else if (IotModbusCommonUtils.isWriteResponse(fc)) { + // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节 + // 已有 6 字节,还需 2 字节 + state = STATE_WRITE_BODY; + this.pendingData = Buffer.buffer(); + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节 + parser.fixedSizeMode(2); // 还需 2 字节(CRC) + } else { + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); + resetToHeader(); + } + } + + @Override + public void handle(Buffer buffer) { + switch (state) { + case STATE_HEADER: + handleHeader(buffer); + break; + case STATE_EXCEPTION_BODY: + handleExceptionBody(buffer); + break; + case STATE_READ_BYTE_COUNT: + handleReadByteCount(buffer); + break; + case STATE_READ_DATA: + handleReadData(buffer); + break; + case STATE_WRITE_BODY: + handleWriteBody(buffer); + break; + default: + resetToHeader(); + } + } + + private void handleHeader(Buffer buffer) { + byte[] header = buffer.getBytes(); + this.slaveId = header[0]; + this.functionCode = header[1]; + int fc = functionCode & 0xFF; + if (IotModbusCommonUtils.isExceptionResponse(fc)) { + // 异常响应 + state = STATE_EXCEPTION_BODY; + parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) { + // 请求模式下的读请求:固定 8 字节,已读 2 字节(slaveId + FC),还需 6 字节 + state = STATE_WRITE_BODY; + pendingData = Buffer.buffer(); + parser.fixedSizeMode(6); // StartAddr(2) + Quantity(2) + CRC(2) + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) { + // 读响应或自定义 FC + state = STATE_READ_BYTE_COUNT; + parser.fixedSizeMode(1); // byteCount + } else if (IotModbusCommonUtils.isWriteResponse(fc)) { + // 写响应 + state = STATE_WRITE_BODY; + pendingData = Buffer.buffer(); + parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2) + } else { + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); + resetToHeader(); + } + } + + private void handleExceptionBody(Buffer buffer) { + // buffer = exceptionCode(1) + CRC(2) + Buffer frame = Buffer.buffer(); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendBuffer(buffer); + emitFrame(frame); + resetToHeader(); + } + + private void handleReadByteCount(Buffer buffer) { + this.byteCount = buffer.getByte(0); + int bc = byteCount & 0xFF; + state = STATE_READ_DATA; + pendingData = Buffer.buffer(); + expectedDataLen = bc + 2; // data(bc) + CRC(2) + parser.fixedSizeMode(expectedDataLen); + } + + private void handleReadData(Buffer buffer) { + pendingData.appendBuffer(buffer); + if (pendingData.length() >= expectedDataLen) { + // 组装完整帧 + Buffer frame = Buffer.buffer(); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendByte(byteCount); + frame.appendBuffer(pendingData); + emitFrame(frame); + resetToHeader(); + } + // 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode) + } + + private void handleWriteBody(Buffer buffer) { + pendingData.appendBuffer(buffer); + // 完整帧 + Buffer frame = Buffer.buffer(); + frame.appendByte(slaveId); + frame.appendByte(functionCode); + frame.appendBuffer(pendingData); + emitFrame(frame); + resetToHeader(); + } + + /** + * 发射完整帧:解码并回调 + */ + private void emitFrame(Buffer frameBuffer) { + IotModbusFrame frame = decodeResponse(frameBuffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); + if (frame != null) { + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_RTU); + } + } + + private void resetToHeader() { + state = STATE_HEADER; + pendingData = null; + parser.fixedSizeMode(2); // slaveId + FC + } + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java new file mode 100644 index 000000000..b5c48b225 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java @@ -0,0 +1,210 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec; + +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; + +/** + * IoT Modbus 帧编码器:负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotModbusFrameEncoder { + + private final int customFunctionCode; + + // ==================== 编码 ==================== + + /** + * 编码读请求 + * + * @param slaveId 从站地址 + * @param functionCode 功能码 + * @param startAddress 起始寄存器地址 + * @param quantity 寄存器数量 + * @param format 帧格式 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) + * @return 编码后的字节数组 + */ + public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity, + IotModbusFrameFormatEnum format, Integer transactionId) { + // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)] + byte[] pdu = new byte[5]; + pdu[0] = (byte) functionCode; + pdu[1] = (byte) ((startAddress >> 8) & 0xFF); + pdu[2] = (byte) (startAddress & 0xFF); + pdu[3] = (byte) ((quantity >> 8) & 0xFF); + pdu[4] = (byte) (quantity & 0xFF); + return wrapFrame(slaveId, pdu, format, transactionId); + } + + /** + * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) + * + * @param slaveId 从站地址 + * @param functionCode 功能码 + * @param address 寄存器地址 + * @param value 值 + * @param format 帧格式 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) + * @return 编码后的字节数组 + */ + public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value, + IotModbusFrameFormatEnum format, Integer transactionId) { + // FC05 单写线圈:Modbus 标准要求 value 为 0xFF00(ON)或 0x0000(OFF) + if (functionCode == IotModbusCommonUtils.FC_WRITE_SINGLE_COIL) { + value = (value != 0) ? 0xFF00 : 0x0000; + } + // PDU: [FC(1)] [Address(2)] [Value(2)] + byte[] pdu = new byte[5]; + pdu[0] = (byte) functionCode; + pdu[1] = (byte) ((address >> 8) & 0xFF); + pdu[2] = (byte) (address & 0xFF); + pdu[3] = (byte) ((value >> 8) & 0xFF); + pdu[4] = (byte) (value & 0xFF); + return wrapFrame(slaveId, pdu, format, transactionId); + } + + /** + * 编码写多个寄存器请求(FC16) + * + * @param slaveId 从站地址 + * @param address 起始地址 + * @param values 值数组 + * @param format 帧格式 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) + * @return 编码后的字节数组 + */ + public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values, + IotModbusFrameFormatEnum format, Integer transactionId) { + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)] + int quantity = values.length; + int byteCount = quantity * 2; + byte[] pdu = new byte[6 + byteCount]; + pdu[0] = (byte) 16; // FC16 + pdu[1] = (byte) ((address >> 8) & 0xFF); + pdu[2] = (byte) (address & 0xFF); + pdu[3] = (byte) ((quantity >> 8) & 0xFF); + pdu[4] = (byte) (quantity & 0xFF); + pdu[5] = (byte) byteCount; + for (int i = 0; i < quantity; i++) { + pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF); + pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF); + } + return wrapFrame(slaveId, pdu, format, transactionId); + } + + /** + * 编码写多个线圈请求(FC15) + *

+ * 按 Modbus FC15 标准,线圈值按 bit 打包(每个 byte 包含 8 个线圈状态)。 + * + * @param slaveId 从站地址 + * @param address 起始地址 + * @param values 线圈值数组(int[],非0 表示 ON,0 表示 OFF) + * @param format 帧格式 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) + * @return 编码后的字节数组 + */ + public byte[] encodeWriteMultipleCoilsRequest(int slaveId, int address, int[] values, + IotModbusFrameFormatEnum format, Integer transactionId) { + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [CoilValues(N)] + int quantity = values.length; + int byteCount = (quantity + 7) / 8; // 向上取整 + byte[] pdu = new byte[6 + byteCount]; + pdu[0] = (byte) IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS; // FC15 + pdu[1] = (byte) ((address >> 8) & 0xFF); + pdu[2] = (byte) (address & 0xFF); + pdu[3] = (byte) ((quantity >> 8) & 0xFF); + pdu[4] = (byte) (quantity & 0xFF); + pdu[5] = (byte) byteCount; + // 按 bit 打包:每个 byte 的 bit0 对应最低地址的线圈 + for (int i = 0; i < quantity; i++) { + if (values[i] != 0) { + pdu[6 + i / 8] |= (byte) (1 << (i % 8)); + } + } + return wrapFrame(slaveId, pdu, format, transactionId); + } + + /** + * 编码自定义功能码帧(认证响应等) + * + * @param slaveId 从站地址 + * @param jsonData JSON 数据 + * @param format 帧格式 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) + * @return 编码后的字节数组 + */ + public byte[] encodeCustomFrame(int slaveId, String jsonData, + IotModbusFrameFormatEnum format, Integer transactionId) { + byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8); + // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)] + byte[] pdu = new byte[2 + jsonBytes.length]; + pdu[0] = (byte) customFunctionCode; + pdu[1] = (byte) jsonBytes.length; + System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length); + return wrapFrame(slaveId, pdu, format, transactionId); + } + + // ==================== 帧封装 ==================== + + /** + * 将 PDU 封装为完整帧 + * + * @param slaveId 从站地址 + * @param pdu PDU 数据(含 functionCode) + * @param format 帧格式 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式可为 null) + * @return 完整帧字节数组 + */ + private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, Integer transactionId) { + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { + return wrapTcpFrame(slaveId, pdu, transactionId != null ? transactionId : 0); + } else { + return wrapRtuFrame(slaveId, pdu); + } + } + + /** + * 封装 MODBUS_TCP 帧 + * [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...] + */ + private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) { + int length = 1 + pdu.length; // UnitId + PDU + byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU + // MBAP Header + frame[0] = (byte) ((transactionId >> 8) & 0xFF); + frame[1] = (byte) (transactionId & 0xFF); + frame[2] = 0; // Protocol ID high + frame[3] = 0; // Protocol ID low + frame[4] = (byte) ((length >> 8) & 0xFF); + frame[5] = (byte) (length & 0xFF); + // Unit ID + frame[6] = (byte) slaveId; + // PDU + System.arraycopy(pdu, 0, frame, 7, pdu.length); + return frame; + } + + /** + * 封装 MODBUS_RTU 帧 + * [SlaveId(1)] [PDU...] [CRC(2)] + */ + private byte[] wrapRtuFrame(int slaveId, byte[] pdu) { + byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC + frame[0] = (byte) slaveId; + System.arraycopy(pdu, 0, frame, 1, pdu.length); + // 计算并追加 CRC16 + int crc = IotModbusCommonUtils.calculateCrc16(frame, frame.length - 2); + frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low + frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High + return frame; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java new file mode 100644 index 000000000..eb4683fc8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java @@ -0,0 +1,152 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * IoT Modbus TCP Server 下行消息处理器 + *

+ * 负责: + * 1. 处理下行消息(如属性设置 thing.service.property.set) + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpServerDownstreamHandler { + + private final IotModbusTcpServerConnectionManager connectionManager; + private final IotModbusTcpServerConfigCacheService configCacheService; + private final IotModbusFrameEncoder frameEncoder; + + /** + * TCP 事务 ID 自增器(与 PollScheduler 共享) + */ + private final AtomicInteger transactionIdCounter; + + public IotModbusTcpServerDownstreamHandler(IotModbusTcpServerConnectionManager connectionManager, + IotModbusTcpServerConfigCacheService configCacheService, + IotModbusFrameEncoder frameEncoder, + AtomicInteger transactionIdCounter) { + this.connectionManager = connectionManager; + this.configCacheService = configCacheService; + this.frameEncoder = frameEncoder; + this.transactionIdCounter = transactionIdCounter; + } + + /** + * 处理下行消息 + */ + @SuppressWarnings({"unchecked", "DuplicatedCode"}) + public void handle(IotDeviceMessage message) { + // 1.1 检查是否是属性设置消息 + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) { + return; + } + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { + log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); + return; + } + // 1.2 获取设备配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId()); + if (config == null) { + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId()); + return; + } + // 1.3 获取连接信息 + ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); + if (connInfo == null) { + log.warn("[handle][设备 {} 没有连接]", message.getDeviceId()); + return; + } + + // 2. 解析属性值并写入 + Object params = message.getParams(); + if (!(params instanceof Map)) { + log.warn("[handle][params 不是 Map 类型: {}]", params); + return; + } + Map propertyMap = (Map) params; + for (Map.Entry entry : propertyMap.entrySet()) { + String identifier = entry.getKey(); + Object value = entry.getValue(); + // 2.1 查找对应的点位配置 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier); + if (point == null) { + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); + continue; + } + // 2.2 检查是否支持写操作 + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) { + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); + continue; + } + + // 2.3 执行写入 + writeProperty(config.getDeviceId(), connInfo, point, value); + } + } + + /** + * 写入属性值 + */ + private void writeProperty(Long deviceId, ConnectionInfo connInfo, + IotModbusPointRespDTO point, Object value) { + // 1.1 转换属性值为原始值 + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point); + + // 1.2 确定帧格式和事务 ID + IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); + Assert.notNull(frameFormat, "连接帧格式不能为空"); + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP + ? (transactionIdCounter.incrementAndGet() & 0xFFFF) + : null; + int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; + // 1.3 编码写请求 + byte[] data; + int readFunctionCode = point.getFunctionCode(); + Integer writeSingleCode = IotModbusCommonUtils.getWriteSingleFunctionCode(readFunctionCode); + Integer writeMultipleCode = IotModbusCommonUtils.getWriteMultipleFunctionCode(readFunctionCode); + if (rawValues.length == 1 && writeSingleCode != null) { + // 单个值:使用单写功能码(FC05/FC06) + data = frameEncoder.encodeWriteSingleRequest(slaveId, writeSingleCode, + point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); + } else if (writeMultipleCode != null) { + // 多个值:使用多写功能码(FC15/FC16) + if (writeMultipleCode == IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS) { + data = frameEncoder.encodeWriteMultipleCoilsRequest(slaveId, + point.getRegisterAddress(), rawValues, frameFormat, transactionId); + } else { + data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, + point.getRegisterAddress(), rawValues, frameFormat, transactionId); + } + } else { + log.warn("[writeProperty][点位 {} 不支持写操作, 功能码={}]", point.getIdentifier(), readFunctionCode); + return; + } + + // 2. 发送消息 + connectionManager.sendToDevice(deviceId, data).onSuccess(v -> + log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", + deviceId, point.getIdentifier(), value) + ).onFailure(e -> + log.error("[writeProperty][写入失败, deviceId={}, identifier={}, value={}]", + deviceId, point.getIdentifier(), value, e) + ); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java new file mode 100644 index 000000000..1d36b69ee --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT Modbus TCP Server 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpServerDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { + + private final IotModbusTcpServerDownstreamHandler downstreamHandler; + + public IotModbusTcpServerDownstreamSubscriber(IotModbusTcpServerProtocol protocol, + IotModbusTcpServerDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java new file mode 100644 index 000000000..db8a9fdfa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java @@ -0,0 +1,280 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.net.NetSocket; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +/** + * IoT Modbus TCP Server 上行数据处理器 + *

+ * 处理: + * 1. 自定义 FC 认证 + * 2. 轮询响应 → 点位翻译 → thing.property.post + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpServerUpstreamHandler { + + private static final String METHOD_AUTH = "auth"; + + private final IotDeviceCommonApi deviceApi; + private final IotDeviceMessageService messageService; + private final IotModbusFrameEncoder frameEncoder; + private final IotModbusTcpServerConnectionManager connectionManager; + private final IotModbusTcpServerConfigCacheService configCacheService; + private final IotModbusTcpServerPendingRequestManager pendingRequestManager; + private final IotModbusTcpServerPollScheduler pollScheduler; + private final IotDeviceService deviceService; + + private final String serverId; + + public IotModbusTcpServerUpstreamHandler(IotDeviceCommonApi deviceApi, + IotDeviceMessageService messageService, + IotModbusFrameEncoder frameEncoder, + IotModbusTcpServerConnectionManager connectionManager, + IotModbusTcpServerConfigCacheService configCacheService, + IotModbusTcpServerPendingRequestManager pendingRequestManager, + IotModbusTcpServerPollScheduler pollScheduler, + IotDeviceService deviceService, + String serverId) { + this.deviceApi = deviceApi; + this.messageService = messageService; + this.frameEncoder = frameEncoder; + this.connectionManager = connectionManager; + this.configCacheService = configCacheService; + this.pendingRequestManager = pendingRequestManager; + this.pollScheduler = pollScheduler; + this.deviceService = deviceService; + this.serverId = serverId; + } + + // ========== 帧处理入口 ========== + + /** + * 处理帧 + */ + public void handleFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { + if (frame == null) { + return; + } + // 1. 异常响应 + if (frame.isException()) { + log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]", + frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode()); + return; + } + + // 2. 情况一:自定义功能码(认证等扩展) + if (StrUtil.isNotEmpty(frame.getCustomData())) { + handleCustomFrame(socket, frame, frameFormat); + return; + } + + // 3. 情况二:标准 Modbus 响应 → 轮询响应处理 + handlePollingResponse(socket, frame, frameFormat); + } + + // ========== 自定义 FC 处理(认证等) ========== + + /** + * 处理自定义功能码帧 + *

+ * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler} + */ + private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { + String method = null; + try { + IotDeviceMessage message = JsonUtils.parseObject(frame.getCustomData(), IotDeviceMessage.class); + if (message == null) { + throw invalidParamException("自定义 FC 数据解析失败"); + } + method = message.getMethod(); + if (METHOD_AUTH.equals(method)) { + handleAuth(socket, frame, frameFormat, message.getParams()); + return; + } + log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]", + method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData()); + } catch (ServiceException e) { + // 已知业务异常,返回对应的错误码和错误信息 + sendCustomResponse(socket, frame, frameFormat, method, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + // 参数校验异常,返回 400 错误 + sendCustomResponse(socket, frame, frameFormat, method, BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + // 其他未知异常,返回 500 错误 + log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]", + frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e); + sendCustomResponse(socket, frame, frameFormat, method, + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + } + + /** + * 处理认证请求 + */ + @SuppressWarnings("DataFlowIssue") + private void handleAuth(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat, Object params) { + // 1. 解析认证参数 + IotDeviceAuthReqDTO request = JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); + Assert.notNull(request, "认证参数不能为空"); + Assert.notBlank(request.getUsername(), "username 不能为空"); + Assert.notBlank(request.getPassword(), "password 不能为空"); + // 特殊:考虑到 modbus 消息体积较小,默认 clientId 传递空串 + if (StrUtil.isBlank(request.getClientId())) { + request.setClientId(IotDeviceAuthUtils.buildClientIdFromUsername(request.getUsername())); + } + Assert.notBlank(request.getClientId(), "clientId 不能为空"); + + // 2.1 调用认证 API + CommonResult result = deviceApi.authDevice(request); + result.checkError(); + if (BooleanUtil.isFalse(result.getData())) { + log.warn("[handleAuth][认证失败, clientId={}, username={}]", request.getClientId(), request.getUsername()); + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "认证失败"); + return; + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(request.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + // 2.4 加载设备 Modbus 配置,无配置则阻断认证 + IotModbusDeviceConfigRespDTO modbusConfig = configCacheService.loadDeviceConfig(device.getId()); + if (modbusConfig == null) { + log.warn("[handleAuth][设备 {} 没有 Modbus 点位配置, 拒绝认证]", device.getId()); + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "设备无 Modbus 配置"); + return; + } + // 2.5 协议不一致,阻断认证 + if (ObjUtil.notEqual(frameFormat.getFormat(), modbusConfig.getFrameFormat())) { + log.warn("[handleAuth][设备 {} frameFormat 不一致, 连接协议={}, 设备配置={},拒绝认证]", + device.getId(), frameFormat.getFormat(), modbusConfig.getFrameFormat()); + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), + "frameFormat 协议不一致"); + return; + } + + // 3.1 注册连接 + ConnectionInfo connectionInfo = new ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(deviceInfo.getProductKey()) + .setDeviceName(deviceInfo.getDeviceName()) + .setSlaveId(frame.getSlaveId()) + .setFrameFormat(frameFormat); + connectionManager.registerConnection(socket, connectionInfo); + // 3.2 发送上线消息 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + messageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); + // 3.3 发送成功响应 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, + GlobalErrorCodeConstants.SUCCESS.getCode(), "success"); + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", request.getClientId(), device.getId()); + + // 4. 启动轮询 + pollScheduler.updatePolling(modbusConfig); + } + + /** + * 发送自定义功能码响应 + */ + private void sendCustomResponse(NetSocket socket, IotModbusFrame frame, + IotModbusFrameFormatEnum frameFormat, + String method, int code, String message) { + Map response = MapUtil.builder() + .put("method", method) + .put("code", code) + .put("message", message) + .build(); + byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response), + frameFormat, frame.getTransactionId()); + connectionManager.sendToSocket(socket, data); + } + + // ========== 轮询响应处理 ========== + + /** + * 处理轮询响应(云端轮询模式) + */ + private void handlePollingResponse(NetSocket socket, IotModbusFrame frame, + IotModbusFrameFormatEnum frameFormat) { + // 1. 获取连接信息(未认证连接丢弃) + ConnectionInfo info = connectionManager.getConnectionInfo(socket); + if (info == null) { + log.warn("[handlePollingResponse][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); + return; + } + + // 2.1 匹配 PendingRequest + PendingRequest request = pendingRequestManager.matchResponse( + info.getDeviceId(), frame, frameFormat); + if (request == null) { + log.debug("[handlePollingResponse][未匹配到 PendingRequest, deviceId={}, FC={}]", + info.getDeviceId(), frame.getFunctionCode()); + return; + } + // 2.2 提取寄存器值 + int[] rawValues = IotModbusCommonUtils.extractValues(frame); + if (rawValues == null) { + log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", + info.getDeviceId(), request.getIdentifier()); + return; + } + // 2.3 查找点位配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, request.getPointId()); + if (point == null) { + return; + } + + // 3.1 转换原始值为物模型属性值(点位翻译) + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValues, point); + // 3.2 构造属性上报消息 + Map params = MapUtil.of(request.getIdentifier(), convertedValue); + IotDeviceMessage message = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); + + // 4. 发送到消息总线 + messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId); + log.debug("[handlePollingResponse][设备={}, 属性={}, 原始值={}, 转换值={}]", + info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java new file mode 100644 index 000000000..791a7c7a9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java @@ -0,0 +1,118 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT Modbus TCP Server 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotModbusTcpServerConfigCacheService { + + private final IotDeviceCommonApi deviceApi; + + /** + * 配置缓存:deviceId -> 配置 + */ + private final Map configCache = new ConcurrentHashMap<>(); + + /** + * 加载单个设备的配置(认证成功后调用) + * + * @param deviceId 设备 ID + * @return 设备配置 + */ + public IotModbusDeviceConfigRespDTO loadDeviceConfig(Long deviceId) { + try { + // 1. 从远程 API 获取配置 + IotModbusDeviceConfigListReqDTO reqDTO = new IotModbusDeviceConfigListReqDTO() + .setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setMode(IotModbusModeEnum.POLLING.getMode()) + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType()) + .setDeviceIds(Collections.singleton(deviceId)); + CommonResult> result = deviceApi.getModbusDeviceConfigList(reqDTO); + result.checkError(); + IotModbusDeviceConfigRespDTO modbusConfig = CollUtil.getFirst(result.getData()); + if (modbusConfig == null) { + log.warn("[loadDeviceConfig][远程获取配置失败,未找到数据, deviceId={}]", deviceId); + return null; + } + + // 2. 更新缓存并返回 + configCache.put(modbusConfig.getDeviceId(), modbusConfig); + return modbusConfig; + } catch (Exception e) { + log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e); + return null; + } + } + + /** + * 刷新已连接设备的配置缓存 + *

+ * 定时调用,从远程 API 拉取最新配置,只更新已连接设备的缓存。 + * + * @param connectedDeviceIds 当前已连接的设备 ID 集合 + * @return 已连接设备的最新配置列表 + */ + public List refreshConnectedDeviceConfigList(Set connectedDeviceIds) { + if (CollUtil.isEmpty(connectedDeviceIds)) { + return Collections.emptyList(); + } + try { + // 1. 从远程获取已连接设备的配置 + CommonResult> result = deviceApi.getModbusDeviceConfigList( + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setMode(IotModbusModeEnum.POLLING.getMode()) + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType()) + .setDeviceIds(connectedDeviceIds)); + List modbusConfigs = result.getCheckedData(); + + // 2. 更新缓存并返回 + for (IotModbusDeviceConfigRespDTO config : modbusConfigs) { + configCache.put(config.getDeviceId(), config); + } + return modbusConfigs; + } catch (Exception e) { + log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e); + return null; + } + } + + /** + * 获取设备配置 + */ + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) { + IotModbusDeviceConfigRespDTO config = configCache.get(deviceId); + if (config != null) { + return config; + } + // 缓存未命中,从远程 API 获取 + return loadDeviceConfig(deviceId); + } + + /** + * 移除设备配置缓存(设备断连时调用) + */ + public void removeConfig(Long deviceId) { + configCache.remove(deviceId); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java new file mode 100644 index 000000000..781d8ac54 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java @@ -0,0 +1,174 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; + +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetSocket; +import lombok.Data; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT Modbus TCP Server 连接管理器 + *

+ * 管理设备 TCP 连接:socket ↔ 设备双向映射 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpServerConnectionManager { + + /** + * socket → 连接信息 + */ + private final Map connectionMap = new ConcurrentHashMap<>(); + + /** + * deviceId → socket + */ + private final Map deviceSocketMap = new ConcurrentHashMap<>(); + + /** + * 连接信息 + */ + @Data + @Accessors(chain = true) + public static class ConnectionInfo { + + /** + * 设备编号 + */ + private Long deviceId; + /** + * 产品标识 + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + /** + * 从站地址 + */ + private Integer slaveId; + + /** + * 帧格式(首帧自动检测得到) + */ + private IotModbusFrameFormatEnum frameFormat; + + } + + /** + * 注册已认证的连接 + */ + public void registerConnection(NetSocket socket, ConnectionInfo info) { + // 先检查该设备是否有旧连接,若有且不是同一个 socket,关闭旧 socket + NetSocket oldSocket = deviceSocketMap.get(info.getDeviceId()); + if (oldSocket != null && oldSocket != socket) { + log.info("[registerConnection][设备 {} 存在旧连接, 关闭旧 socket, oldRemote={}, newRemote={}]", + info.getDeviceId(), oldSocket.remoteAddress(), socket.remoteAddress()); + connectionMap.remove(oldSocket); + try { + oldSocket.close(); + } catch (Exception e) { + log.warn("[registerConnection][关闭旧 socket 失败, deviceId={}, oldRemote={}]", + info.getDeviceId(), oldSocket.remoteAddress(), e); + } + } + + // 注册新连接 + connectionMap.put(socket, info); + deviceSocketMap.put(info.getDeviceId(), socket); + log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]", + info.getDeviceId(), socket.remoteAddress()); + } + + /** + * 获取连接信息 + */ + public ConnectionInfo getConnectionInfo(NetSocket socket) { + return connectionMap.get(socket); + } + + /** + * 根据设备 ID 获取连接信息 + */ + public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) { + NetSocket socket = deviceSocketMap.get(deviceId); + return socket != null ? connectionMap.get(socket) : null; + } + + /** + * 获取所有已连接设备的 ID 集合 + */ + public Set getConnectedDeviceIds() { + return deviceSocketMap.keySet(); + } + + /** + * 移除连接 + */ + public ConnectionInfo removeConnection(NetSocket socket) { + ConnectionInfo info = connectionMap.remove(socket); + if (info != null && info.getDeviceId() != null) { + // 使用两参数 remove:只有当 deviceSocketMap 中对应的 socket 就是当前 socket 时才删除, + // 避免新 socket 已注册后旧 socket 关闭时误删新映射 + boolean removed = deviceSocketMap.remove(info.getDeviceId(), socket); + if (removed) { + log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId()); + } else { + log.info("[removeConnection][设备 {} 旧连接关闭, 新连接仍在线, 跳过清理]", info.getDeviceId()); + } + } + return info; + } + + /** + * 发送数据到设备 + * + * @return 发送结果 Future + */ + public Future sendToDevice(Long deviceId, byte[] data) { + NetSocket socket = deviceSocketMap.get(deviceId); + if (socket == null) { + log.warn("[sendToDevice][设备 {} 没有连接]", deviceId); + return Future.failedFuture("设备 " + deviceId + " 没有连接"); + } + return sendToSocket(socket, data); + } + + /** + * 发送数据到指定 socket + * + * @return 发送结果 Future + */ + public Future sendToSocket(NetSocket socket, byte[] data) { + return socket.write(Buffer.buffer(data)); + } + + /** + * 关闭所有连接 + */ + public void closeAll() { + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 + List sockets = new ArrayList<>(connectionMap.keySet()); + connectionMap.clear(); + deviceSocketMap.clear(); + // 2. 关闭所有 socket(closeHandler 中 removeConnection 发现 map 为空会安全跳过) + for (NetSocket socket : sockets) { + try { + socket.close(); + } catch (Exception e) { + log.error("[closeAll][关闭连接失败]", e); + } + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java new file mode 100644 index 000000000..9bac7f86d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java @@ -0,0 +1,154 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Deque; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; + +/** + * IoT Modbus TCP Server 待响应请求管理器 + *

+ * 管理轮询下发的请求,用于匹配设备响应: + * - TCP 模式:按 transactionId 精确匹配 + * - RTU 模式:按 slaveId + functionCode FIFO 匹配 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpServerPendingRequestManager { + + /** + * deviceId → 有序队列 + */ + private final Map> pendingRequests = new ConcurrentHashMap<>(); + + /** + * 待响应请求信息 + */ + @Data + @AllArgsConstructor + public static class PendingRequest { + + private Long deviceId; + private Long pointId; + private String identifier; + private int slaveId; + private int functionCode; + private int registerAddress; + private int registerCount; + private Integer transactionId; + private long expireAt; + + } + + /** + * 添加待响应请求 + */ + public void addRequest(PendingRequest request) { + pendingRequests.computeIfAbsent(request.getDeviceId(), k -> new ConcurrentLinkedDeque<>()) + .addLast(request); + } + + /** + * 匹配响应(TCP 模式按 transactionId,RTU 模式按 FIFO) + * + * @param deviceId 设备 ID + * @param frame 收到的响应帧 + * @param frameFormat 帧格式 + * @return 匹配到的 PendingRequest,没有匹配返回 null + */ + public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame, + IotModbusFrameFormatEnum frameFormat) { + Deque queue = pendingRequests.get(deviceId); + if (CollUtil.isEmpty(queue)) { + return null; + } + + // TCP 模式:按 transactionId 精确匹配 + if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) { + return matchByTransactionId(queue, frame.getTransactionId()); + } + // RTU 模式:FIFO,匹配 slaveId + functionCode + registerCount + int responseRegisterCount = IotModbusCommonUtils.extractRegisterCountFromResponse(frame); + return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode(), responseRegisterCount); + } + + /** + * 按 transactionId 匹配 + */ + private PendingRequest matchByTransactionId(Deque queue, int transactionId) { + Iterator it = queue.iterator(); + while (it.hasNext()) { + PendingRequest req = it.next(); + if (req.getTransactionId() != null && req.getTransactionId() == transactionId) { + it.remove(); + return req; + } + } + return null; + } + + /** + * 按 FIFO 匹配(slaveId + functionCode + registerCount) + */ + private PendingRequest matchByFifo(Deque queue, int slaveId, int functionCode, + int responseRegisterCount) { + Iterator it = queue.iterator(); + while (it.hasNext()) { + PendingRequest req = it.next(); + if (req.getSlaveId() == slaveId + && req.getFunctionCode() == functionCode + && (responseRegisterCount <= 0 || req.getRegisterCount() == responseRegisterCount)) { + it.remove(); + return req; + } + } + return null; + } + + /** + * 清理过期请求 + */ + public void cleanupExpired() { + long now = System.currentTimeMillis(); + for (Map.Entry> entry : pendingRequests.entrySet()) { + Deque queue = entry.getValue(); + int removed = 0; + Iterator it = queue.iterator(); + while (it.hasNext()) { + PendingRequest req = it.next(); + if (req.getExpireAt() < now) { + it.remove(); + removed++; + } + } + if (removed > 0) { + log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed); + } + } + } + + /** + * 清理指定设备的所有待响应请求 + */ + public void removeDevice(Long deviceId) { + pendingRequests.remove(deviceId); + } + + /** + * 清理所有待响应请求 + */ + public void clear() { + pendingRequests.clear(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java new file mode 100644 index 000000000..4660f8e7e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java @@ -0,0 +1,111 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest; +import io.vertx.core.Vertx; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * IoT Modbus TCP Server 轮询调度器:编码读请求帧,通过 TCP 连接发送到设备,注册 PendingRequest 等待响应 + * + * @author 芋道源码 + */ +@Slf4j +public class IotModbusTcpServerPollScheduler extends AbstractIotModbusPollScheduler { + + private final IotModbusTcpServerConnectionManager connectionManager; + private final IotModbusFrameEncoder frameEncoder; + private final IotModbusTcpServerPendingRequestManager pendingRequestManager; + private final IotModbusTcpServerConfigCacheService configCacheService; + private final int requestTimeout; + /** + * TCP 事务 ID 自增器(与 DownstreamHandler 共享) + */ + @Getter + private final AtomicInteger transactionIdCounter; + + public IotModbusTcpServerPollScheduler(Vertx vertx, + IotModbusTcpServerConnectionManager connectionManager, + IotModbusFrameEncoder frameEncoder, + IotModbusTcpServerPendingRequestManager pendingRequestManager, + int requestTimeout, + AtomicInteger transactionIdCounter, + IotModbusTcpServerConfigCacheService configCacheService) { + super(vertx); + this.connectionManager = connectionManager; + this.frameEncoder = frameEncoder; + this.pendingRequestManager = pendingRequestManager; + this.requestTimeout = requestTimeout; + this.transactionIdCounter = transactionIdCounter; + this.configCacheService = configCacheService; + } + + // ========== 轮询执行 ========== + + /** + * 轮询单个点位 + */ + @Override + @SuppressWarnings("DuplicatedCode") + protected void pollPoint(Long deviceId, Long pointId) { + // 1.1 从 configCache 获取最新配置 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); + if (config == null || CollUtil.isEmpty(config.getPoints())) { + log.warn("[pollPoint][设备 {} 没有配置]", deviceId); + return; + } + // 1.2 查找点位 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId); + if (point == null) { + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); + return; + } + + // 2.1 获取连接 + ConnectionInfo connection = connectionManager.getConnectionInfoByDeviceId(deviceId); + if (connection == null) { + log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId); + return; + } + // 2.2 获取 slave ID + IotModbusFrameFormatEnum frameFormat = connection.getFrameFormat(); + Assert.notNull(frameFormat, "设备 {} 的帧格式不能为空", deviceId); + Integer slaveId = connection.getSlaveId(); + Assert.notNull(connection.getSlaveId(), "设备 {} 的 slaveId 不能为空", deviceId); + + // 3.1 编码读请求 + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP + ? (transactionIdCounter.incrementAndGet() & 0xFFFF) + : null; + byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); + // 3.2 注册 PendingRequest + PendingRequest pendingRequest = new PendingRequest( + deviceId, point.getId(), point.getIdentifier(), + slaveId, point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount(), + transactionId, + System.currentTimeMillis() + requestTimeout); + pendingRequestManager.addRequest(pendingRequest); + // 3.3 发送读请求 + connectionManager.sendToDevice(deviceId, data).onSuccess(v -> + log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]", + deviceId, point.getIdentifier(), point.getFunctionCode(), + point.getRegisterAddress(), point.getRegisterCount()) + ).onFailure(e -> + log.warn("[pollPoint][发送失败, 设备={}, 点位={}]", deviceId, point.getIdentifier(), e) + ); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java new file mode 100644 index 000000000..c15087027 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java @@ -0,0 +1,6 @@ +/** + * Modbus TCP Server(从站)协议:设备主动连接网关,自定义 FC65 认证后由网关云端轮询 + *

+ * TCP Server 模式,支持 MODBUS_TCP / MODBUS_RTU 帧格式自动检测 + */ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java new file mode 100644 index 000000000..afbdd93b3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT 网关 MQTT 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotMqttConfig { + + /** + * 最大消息大小(字节) + */ + @NotNull(message = "最大消息大小不能为空") + @Min(value = 1024, message = "最大消息大小不能小于 1024 字节") + private Integer maxMessageSize = 8192; + + /** + * 连接超时时间(秒) + */ + @NotNull(message = "连接超时时间不能为空") + @Min(value = 1, message = "连接超时时间不能小于 1 秒") + private Integer connectTimeoutSeconds = 60; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java deleted file mode 100644 index 3b62368fd..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java +++ /dev/null @@ -1,79 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; -import jakarta.annotation.PostConstruct; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 MQTT 协议:下行消息订阅器 - *

- * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttDownstreamSubscriber implements IotMessageSubscriber { - - private final IotMqttUpstreamProtocol upstreamProtocol; - - private final IotMqttDownstreamHandler downstreamHandler; - - private final IotMessageBus messageBus; - - public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol upstreamProtocol, - IotMqttDownstreamHandler downstreamHandler, - IotMessageBus messageBus) { - this.upstreamProtocol = upstreamProtocol; - this.downstreamHandler = downstreamHandler; - this.messageBus = messageBus; - } - - @PostConstruct - public void subscribe() { - messageBus.register(this); - log.info("[subscribe][MQTT 协议下行消息订阅成功,主题:{}]", getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - try { - // 1. 校验 - String method = message.getMethod(); - if (method == null) { - log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", - message.getId(), message.getDeviceId()); - return; - } - - // 2. 委托给下行处理器处理业务逻辑 - boolean success = downstreamHandler.handleDownstreamMessage(message); - if (success) { - log.debug("[onMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } else { - log.warn("[onMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId()); - } - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", - message.getId(), message.getMethod(), message.getDeviceId(), e); - } - } -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java new file mode 100644 index 000000000..354cd1b45 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java @@ -0,0 +1,344 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Vertx; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.mqtt.MqttEndpoint; +import io.vertx.mqtt.MqttServer; +import io.vertx.mqtt.MqttServerOptions; +import io.vertx.mqtt.MqttTopicSubscription; +import io.vertx.mqtt.messages.MqttPublishMessage; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +/** + * IoT 网关 MQTT 协议:接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttProtocol implements IotProtocol { + + /** + * 注册连接的 clientId 标识 + * + * @see #handleEndpoint(MqttEndpoint) + */ + private static final String AUTH_TYPE_REGISTER = "|authType=register|"; + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * MQTT 服务器 + */ + private MqttServer mqttServer; + + /** + * 连接管理器 + */ + private final IotMqttConnectionManager connectionManager; + /** + * 下行消息订阅者 + */ + private IotMqttDownstreamSubscriber downstreamSubscriber; + + private final IotDeviceMessageService deviceMessageService; + + private final IotMqttAuthHandler authHandler; + private final IotMqttRegisterHandler registerHandler; + private final IotMqttUpstreamHandler upstreamHandler; + + public IotMqttProtocol(ProtocolProperties properties) { + IotMqttConfig mqttConfig = properties.getMqtt(); + Assert.notNull(mqttConfig, "MQTT 协议配置(mqtt)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化连接管理器 + this.connectionManager = new IotMqttConnectionManager(); + + // 初始化 Handler + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService); + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.MQTT; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT MQTT 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建服务器选项 + IotMqttConfig mqttConfig = properties.getMqtt(); + MqttServerOptions options = new MqttServerOptions() + .setPort(properties.getPort()) + .setMaxMessageSize(mqttConfig.getMaxMessageSize()) + .setTimeoutOnConnect(mqttConfig.getConnectTimeoutSeconds()); + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + + // 1.3 创建服务器并设置连接处理器 + mqttServer = MqttServer.create(vertx, options); + mqttServer.endpointHandler(this::handleEndpoint); + + // 1.4 启动 MQTT 服务器 + try { + mqttServer.listen().result(); + running = true; + log.info("[start][IoT MQTT 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager); + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus); + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e); + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 关闭所有连接 + connectionManager.closeAll(); + // 2.2 关闭 MQTT 服务器 + if (mqttServer != null) { + try { + mqttServer.close().result(); + log.info("[stop][IoT MQTT 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} 服务器停止失败]", getId(), e); + } + mqttServer = null; + } + // 2.3 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT MQTT 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT MQTT 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT MQTT 协议 {} 已停止]", getId()); + } + + // ======================================= MQTT 连接处理 ====================================== + + /** + * 处理 MQTT 连接端点 + * + * @param endpoint MQTT 连接端点 + */ + private void handleEndpoint(MqttEndpoint endpoint) { + // 1. 如果是注册请求,注册待认证连接;否则走正常认证流程 + String clientId = endpoint.clientIdentifier(); + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) { + // 情况一:设备注册请求 + registerHandler.handleRegister(endpoint); + return; + } else { + // 情况二:普通认证请求 + if (!authHandler.handleAuthenticationRequest(endpoint)) { + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); + return; + } + } + + // 2.1 设置异常和关闭处理器 + endpoint.exceptionHandler(ex -> { + log.warn("[handleEndpoint][连接异常,客户端 ID: {},地址: {},异常: {}]", + clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage()); + endpoint.close(); + }); + endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等) + endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文 + log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); + cleanupConnection(endpoint); + }); + // 2.2 设置心跳处理器 + endpoint.pingHandler(v -> log.debug("[handleEndpoint][收到客户端心跳,客户端 ID: {}]", clientId)); + + // 3.1 设置消息处理器 + endpoint.publishHandler(message -> processMessage(endpoint, message)); + // 3.2 设置 QoS 2 消息的 PUBREL 处理器 + endpoint.publishReleaseHandler(endpoint::publishComplete); + + // 4.1 设置订阅处理器(带 ACL 校验) + endpoint.subscribeHandler(subscribe -> { + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + List grantedQoSLevels = new ArrayList<>(); + for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) { + String topicName = sub.topicName(); + // 校验主题是否属于当前设备 + if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed( + topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) { + grantedQoSLevels.add(sub.qualityOfService()); + log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName); + } else { + log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName); + grantedQoSLevels.add(MqttQoS.FAILURE); + } + } + endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); + }); + // 4.2 设置取消订阅处理器 + endpoint.unsubscribeHandler(unsubscribe -> { + log.debug("[handleEndpoint][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); + }); + + // 5. 接受连接 + endpoint.accept(false); + } + + /** + * 处理消息(发布) + * + * @param endpoint MQTT 连接端点 + * @param message 发布消息 + */ + private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { + String clientId = endpoint.clientIdentifier(); + try { + // 1. 处理业务消息 + String topic = message.topicName(); + byte[] payload = message.payload().getBytes(); + upstreamHandler.handleBusinessRequest(endpoint, topic, payload); + + // 2. 根据 QoS 级别发送相应的确认消息 + handleQoSAck(endpoint, message); + } catch (Exception e) { + log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); + endpoint.close(); + } + } + + /** + * 处理 QoS 确认 + * + * @param endpoint MQTT 连接端点 + * @param message 发布消息 + */ + private void handleQoSAck(MqttEndpoint endpoint, MqttPublishMessage message) { + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { + // QoS 1: 发送 PUBACK 确认 + endpoint.publishAcknowledge(message.messageId()); + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { + // QoS 2: 发送 PUBREC 确认 + endpoint.publishReceived(message.messageId()); + } + // QoS 0 无需确认 + } + + /** + * 清理连接 + * + * @param endpoint MQTT 连接端点 + */ + private void cleanupConnection(MqttEndpoint endpoint) { + try { + // 1. 发送设备离线消息 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + } + + // 2. 注销连接 + connectionManager.unregisterConnection(endpoint); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", + endpoint.clientIdentifier(), e.getMessage()); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java deleted file mode 100644 index fc0b6672c..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java +++ /dev/null @@ -1,92 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; - -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.mqtt.MqttServer; -import io.vertx.mqtt.MqttServerOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 MQTT 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttUpstreamProtocol { - - private final IotGatewayProperties.MqttProperties mqttProperties; - - private final IotDeviceMessageService messageService; - - private final IotMqttConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private MqttServer mqttServer; - - public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties, - IotDeviceMessageService messageService, - IotMqttConnectionManager connectionManager, - Vertx vertx) { - this.mqttProperties = mqttProperties; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort()); - } - - // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈; - @PostConstruct - public void start() { - // 创建服务器选项 - MqttServerOptions options = new MqttServerOptions() - .setPort(mqttProperties.getPort()) - .setMaxMessageSize(mqttProperties.getMaxMessageSize()) - .setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds()); - - // 配置 SSL(如果启用) - if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) { - options.setSsl(true) - .setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions()) - .setTrustOptions(mqttProperties.getSslOptions().getTrustOptions()); - } - - // 创建服务器并设置连接处理器 - mqttServer = MqttServer.create(vertx, options); - mqttServer.endpointHandler(endpoint -> { - IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager); - handler.handle(endpoint); - }); - - // 启动服务器 - try { - mqttServer.listen().result(); - log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 MQTT 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (mqttServer != null) { - try { - mqttServer.close().result(); - log.info("[stop][IoT 网关 MQTT 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 MQTT 协议停止失败]", e); - } - } - } -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java new file mode 100644 index 000000000..18a5a413d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议:下行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotMqttDownstreamHandler { + + private final IotDeviceMessageService deviceMessageService; + + private final IotMqttConnectionManager connectionManager; + + /** + * 处理下行消息 + * + * @param message 设备消息 + */ + public void handle(IotDeviceMessage message) { + try { + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + + // 1. 检查设备连接 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( + message.getDeviceId()); + if (connectionInfo == null) { + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + return; + } + + // 2.1 序列化消息 + byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName()); + Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空"); + // 2.2 构建主题 + Assert.notBlank(message.getMethod(), "消息方法不能为空"); + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); + String topic = IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), isReply); + Assert.notBlank(topic, "主题不能为空"); + + // 3. 发送到设备 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, + MqttQoS.AT_LEAST_ONCE.value(), false); + if (!success) { + throw new RuntimeException("下行消息发送失败"); + } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},主题: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), topic, payload.length); + } catch (Exception e) { + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", + message.getDeviceId(), message.getMethod(), message, e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java new file mode 100644 index 000000000..5f0b547f1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { + + private final IotMqttDownstreamHandler downstreamHandler; + + public IotMqttDownstreamSubscriber(IotMqttProtocol protocol, + IotMqttDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java new file mode 100644 index 000000000..443dc1069 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttEndpoint; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议的处理器抽象基类 + *

+ * 提供通用的连接校验、响应发送等功能 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public abstract class IotMqttAbstractHandler { + + protected final IotMqttConnectionManager connectionManager; + protected final IotDeviceMessageService deviceMessageService; + + /** + * 发送成功响应到设备 + * + * @param endpoint MQTT 连接端点 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param requestId 请求 ID + * @param method 方法名 + * @param data 响应数据 + */ + @SuppressWarnings("SameParameterValue") + protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, + String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); + writeResponse(endpoint, productKey, deviceName, method, responseMessage); + } + + /** + * 发送错误响应到设备 + * + * @param endpoint MQTT 连接端点 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param requestId 请求 ID + * @param method 方法名 + * @param errorCode 错误码 + * @param errorMessage 错误消息 + */ + protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, + String requestId, String method, Integer errorCode, String errorMessage) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); + writeResponse(endpoint, productKey, deviceName, method, responseMessage); + } + + /** + * 写入响应消息到设备 + * + * @param endpoint MQTT 连接端点 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param method 方法名 + * @param responseMessage 响应消息 + */ + private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName, + String method, IotDeviceMessage responseMessage) { + try { + // 1.1 序列化消息(根据设备配置的序列化类型) + byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName); + // 1.2 构建响应主题 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); + + // 2. 发送响应消息 + endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); + log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode()); + } catch (Exception e) { + log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java new file mode 100644 index 000000000..d574fdede --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java @@ -0,0 +1,119 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * IoT 网关 MQTT 认证处理器 + *

+ * 处理 MQTT CONNECT 事件,完成设备认证、连接注册、上线通知 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttAuthHandler extends IotMqttAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + private final IotDeviceService deviceService; + private final String serverId; + + public IotMqttAuthHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService, + IotDeviceCommonApi deviceApi, + String serverId) { + super(connectionManager, deviceMessageService); + this.deviceApi = deviceApi; + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.serverId = serverId; + } + + /** + * 处理 MQTT 连接(认证)请求 + * + * @param endpoint MQTT 连接端点 + * @return 认证是否成功 + */ + @SuppressWarnings("DataFlowIssue") + public boolean handleAuthenticationRequest(MqttEndpoint endpoint) { + String clientId = endpoint.clientIdentifier(); + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; + log.debug("[handleConnect][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", + clientId, username, connectionManager.getEndpointAddress(endpoint)); + + try { + // 1.1 解析认证参数 + Assert.notBlank(clientId, "clientId 不能为空"); + Assert.notBlank(username, "username 不能为空"); + Assert.notBlank(password, "password 不能为空"); + // 1.2 构建认证参数 + IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() + .setClientId(clientId) + .setUsername(username) + .setPassword(password); + + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + authResult.checkError(); + if (BooleanUtil.isFalse(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3.1 注册连接 + registerConnection(endpoint, device, clientId); + // 3.2 发送设备上线消息 + sendOnlineMessage(device); + log.info("[handleConnect][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); + return true; + } catch (Exception e) { + log.warn("[handleConnect][设备认证失败,拒绝连接,客户端 ID: {},用户名: {},错误: {}]", + clientId, username, e.getMessage()); + return false; + } + } + + /** + * 注册连接 + */ + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { + IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()) + .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); + connectionManager.registerConnection(endpoint, connectionInfo); + } + + /** + * 发送设备上线消息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java new file mode 100644 index 000000000..9640fa20b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密) + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttRegisterHandler extends IotMqttAbstractHandler { + + private final IotDeviceCommonApi deviceApi; + + public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService) { + super(connectionManager, deviceMessageService); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + /** + * 处理注册连接 + *

+ * 通过 MQTT 连接的 username 解析设备信息,password 作为签名,直接处理设备注册 + * + * @param endpoint MQTT 连接端点 + * @see 阿里云 - 一型一密 + */ + @SuppressWarnings("DataFlowIssue") + public void handleRegister(MqttEndpoint endpoint) { + String clientId = endpoint.clientIdentifier(); + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(); + String productKey = null; + String deviceName = null; + + try { + // 1.1 校验参数 + Assert.notBlank(clientId, "clientId 不能为空"); + Assert.notBlank(username, "username 不能为空"); + Assert.notBlank(password, "password 不能为空"); + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); + Assert.notNull(deviceInfo, "解析设备信息失败"); + productKey = deviceInfo.getProductKey(); + deviceName = deviceInfo.getDeviceName(); + log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]", + clientId, productKey, deviceName); + // 1.2 构建注册参数 + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO() + .setProductKey(productKey) + .setDeviceName(deviceName) + .setSign(password); + + // 2. 调用动态注册 API + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 接受连接,并发送成功响应 + endpoint.accept(false); + sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData()); + log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId); + } catch (Exception e) { + log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage()); + // 接受连接,并发送错误响应 + endpoint.accept(false); + sendErrorResponse(endpoint, productKey, deviceName, null, method, + INTERNAL_SERVER_ERROR.getCode(), e.getMessage()); + } finally { + // 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接) + endpoint.close(); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java new file mode 100644 index 000000000..81a6ece1a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.vertx.mqtt.MqttEndpoint; +import lombok.extern.slf4j.Slf4j; + + +/** + * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { + + private final String serverId; + + public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager, + IotDeviceMessageService deviceMessageService, + String serverId) { + super(connectionManager, deviceMessageService); + this.serverId = serverId; + } + + /** + * 处理业务消息 + * + * @param endpoint MQTT 连接端点 + * @param topic 主题 + * @param payload 消息内容 + */ + public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { + String clientId = endpoint.clientIdentifier(); + try { + // 1.1 基础检查 + if (ArrayUtil.isEmpty(payload)) { + return; + } + // 1.2 解析主题,获取 productKey 和 deviceName + String[] topicParts = topic.split("/"); + String productKey = ArrayUtil.get(topicParts, 2); + String deviceName = ArrayUtil.get(topicParts, 3); + Assert.notBlank(productKey, "产品 Key 不能为空"); + Assert.notBlank(deviceName, "设备名称不能为空"); + // 1.3 校验设备信息,防止伪造设备消息 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + Assert.notNull(connectionInfo, "无法获取连接信息"); + Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配"); + Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配"); + // 1.4 校验 topic 是否允许发布 + if (!IotMqttTopicUtils.isTopicPublishAllowed(topic, productKey, deviceName)) { + log.warn("[handleBusinessRequest][topic 不允许发布,客户端 ID: {},主题: {}]", clientId, topic); + return; + } + + // 2.1 反序列化消息 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); + if (message == null) { + log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); + return; + } + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) + IotMqttTopicUtils.normalizeReplyMethod(message); + + // 3. 处理业务消息 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic); + } catch (Exception e) { + log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage(), e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java index 0cc2617ee..3d187c847 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java @@ -8,6 +8,8 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -66,7 +68,6 @@ public class IotMqttConnectionManager { } catch (Exception ignored) { // 连接已关闭,忽略异常 } - return realTimeAddress; } @@ -74,24 +75,24 @@ public class IotMqttConnectionManager { * 注册设备连接(包含认证信息) * * @param endpoint MQTT 连接端点 - * @param deviceId 设备 ID * @param connectionInfo 连接信息 */ - public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { + public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) { + Long deviceId = connectionInfo.getDeviceId(); // 如果设备已有其他连接,先清理旧连接 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); if (oldEndpoint != null && oldEndpoint != endpoint) { log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", deviceId, getEndpointAddress(oldEndpoint)); - oldEndpoint.close(); - // 清理旧连接的映射 + // 先清理映射,再关闭连接(避免旧连接处理器干扰) connectionMap.remove(oldEndpoint); + oldEndpoint.close(); } // 注册新连接 connectionMap.put(endpoint, connectionInfo); deviceEndpointMap.put(deviceId, endpoint); - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]", deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); } @@ -129,25 +130,10 @@ public class IotMqttConnectionManager { if (endpoint == null) { return null; } - // 获取连接信息 return getConnectionInfo(endpoint); } - /** - * 检查设备是否在线 - */ - public boolean isDeviceOnline(Long deviceId) { - return deviceEndpointMap.containsKey(deviceId); - } - - /** - * 检查设备是否离线 - */ - public boolean isDeviceOffline(Long deviceId) { - return !isDeviceOnline(deviceId); - } - /** * 发送消息到设备 * @@ -182,6 +168,24 @@ public class IotMqttConnectionManager { return deviceEndpointMap.get(deviceId); } + /** + * 关闭所有连接 + */ + public void closeAll() { + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 + List endpoints = new ArrayList<>(connectionMap.keySet()); + connectionMap.clear(); + deviceEndpointMap.clear(); + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) + for (MqttEndpoint endpoint : endpoints) { + try { + endpoint.close(); + } catch (Exception ignored) { + // 连接可能已关闭,忽略异常 + } + } + } + /** * 连接信息 */ @@ -192,27 +196,15 @@ public class IotMqttConnectionManager { * 设备 ID */ private Long deviceId; - /** * 产品 Key */ private String productKey; - /** * 设备名称 */ private String deviceName; - /** - * 客户端 ID - */ - private String clientId; - - /** - * 是否已认证 - */ - private boolean authenticated; - /** * 连接地址 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java deleted file mode 100644 index c848833f6..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java +++ /dev/null @@ -1,132 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; -import io.netty.handler.codec.mqtt.MqttQoS; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 MQTT 协议:下行消息处理器 - *

- * 专门处理下行消息的业务逻辑,包括: - * 1. 消息编码 - * 2. 主题构建 - * 3. 消息发送 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttDownstreamHandler { - - private final IotDeviceMessageService deviceMessageService; - - private final IotMqttConnectionManager connectionManager; - - public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService, - IotMqttConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.connectionManager = connectionManager; - } - - /** - * 处理下行消息 - * - * @param message 设备消息 - * @return 是否处理成功 - */ - public boolean handleDownstreamMessage(IotDeviceMessage message) { - try { - // 1. 基础校验 - if (message == null || message.getDeviceId() == null) { - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); - return false; - } - - // 2. 检查设备是否在线 - if (connectionManager.isDeviceOffline(message.getDeviceId())) { - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId()); - return false; - } - - // 3. 获取连接信息 - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); - if (connectionInfo == null) { - log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId()); - return false; - } - - // 4. 编码消息 - byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), - connectionInfo.getDeviceName()); - if (payload == null || payload.length == 0) { - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId()); - return false; - } - - // 5. 发送消息到设备 - return sendMessageToDevice(message, connectionInfo, payload); - } catch (Exception e) { - if (message != null) { - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]", - message.getDeviceId(), e.getMessage(), e); - } - return false; - } - } - - /** - * 发送消息到设备 - * - * @param message 设备消息 - * @param connectionInfo 连接信息 - * @param payload 消息负载 - * @return 是否发送成功 - */ - private boolean sendMessageToDevice(IotDeviceMessage message, - IotMqttConnectionManager.ConnectionInfo connectionInfo, - byte[] payload) { - // 1. 构建主题 - String topic = buildDownstreamTopic(message, connectionInfo); - if (StrUtil.isBlank(topic)) { - log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]", - message.getDeviceId(), message.getMethod()); - return false; - } - - // 2. 发送消息 - boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false); - if (success) { - log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]", - message.getDeviceId(), topic, message.getMethod()); - } else { - log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]", - message.getDeviceId(), topic, message.getMethod()); - } - return success; - } - - /** - * 构建下行消息主题 - * - * @param message 设备消息 - * @param connectionInfo 连接信息 - * @return 主题 - */ - private String buildDownstreamTopic(IotDeviceMessage message, - IotMqttConnectionManager.ConnectionInfo connectionInfo) { - String method = message.getMethod(); - if (StrUtil.isBlank(method)) { - return null; - } - - // 使用工具类构建主题,支持回复消息处理 - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); - return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), isReply); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java deleted file mode 100644 index d40dba447..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java +++ /dev/null @@ -1,511 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; -import io.netty.handler.codec.mqtt.MqttConnectReturnCode; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.mqtt.MqttEndpoint; -import io.vertx.mqtt.MqttTopicSubscription; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; -import java.util.Map; - -/** - * MQTT 上行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotMqttUpstreamHandler { - - /** - * 默认编解码类型(MQTT 使用 Alink 协议) - */ - private static final String DEFAULT_CODEC_TYPE = "Alink"; - - /** - * register 请求的 topic 后缀 - */ - private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotMqttConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotMqttConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.connectionManager = connectionManager; - this.serverId = protocol.getServerId(); - } - - /** - * 处理 MQTT 连接 - * - * @param endpoint MQTT 连接端点 - */ - public void handle(MqttEndpoint endpoint) { - String clientId = endpoint.clientIdentifier(); - String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; - String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; - - log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", - clientId, username, connectionManager.getEndpointAddress(endpoint)); - - // 1. 先进行认证 - if (!authenticateDevice(clientId, username, password, endpoint)) { - log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); - return; - } - - log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); - - // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息) - endpoint.pingHandler(v -> { - log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId); - // Vert.x 会自动发送 PINGRESP 响应,无需手动处理 - }); - - // 3. 设置异常和关闭处理器 - endpoint.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint)); - cleanupConnection(endpoint); - }); - endpoint.closeHandler(v -> { - cleanupConnection(endpoint); - }); - - // 4. 设置消息处理器 - endpoint.publishHandler(mqttMessage -> { - try { - // 4.1 根据 topic 判断是否为 register 请求 - String topic = mqttMessage.topicName(); - byte[] payload = mqttMessage.payload().getBytes(); - if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) { - // register 请求:使用默认编解码器处理(设备可能未注册) - processRegisterMessage(clientId, topic, payload, endpoint); - } else { - // 业务请求:正常处理 - processMessage(clientId, topic, payload); - } - - // 4.2 根据 QoS 级别发送相应的确认消息 - if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) { - // QoS 1: 发送 PUBACK 确认 - endpoint.publishAcknowledge(mqttMessage.messageId()); - } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) { - // QoS 2: 发送 PUBREC 确认 - endpoint.publishReceived(mqttMessage.messageId()); - } - // QoS 0 无需确认 - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); - cleanupConnection(endpoint); - endpoint.close(); - } - }); - - // 5. 设置订阅处理器 - endpoint.subscribeHandler(subscribe -> { - // 提取主题名称列表用于日志显示 - List topicNames = subscribe.topicSubscriptions().stream() - .map(MqttTopicSubscription::topicName) - .collect(java.util.stream.Collectors.toList()); - log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); - - // 提取 QoS 列表 - List grantedQoSLevels = subscribe.topicSubscriptions().stream() - .map(MqttTopicSubscription::qualityOfService) - .collect(java.util.stream.Collectors.toList()); - endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); - }); - - // 6. 设置取消订阅处理器 - endpoint.unsubscribeHandler(unsubscribe -> { - log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); - endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); - }); - - // 7. 设置 QoS 2消息的 PUBREL 处理器 - endpoint.publishReleaseHandler(endpoint::publishComplete); - - // 8. 设置断开连接处理器 - endpoint.disconnectHandler(v -> { - log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId); - cleanupConnection(endpoint); - }); - - // 9. 接受连接 - endpoint.accept(false); - } - - /** - * 处理消息 - * - * @param clientId 客户端 ID - * @param topic 主题 - * @param payload 消息内容 - */ - private void processMessage(String clientId, String topic, byte[] payload) { - // 1. 基础检查 - if (payload == null || payload.length == 0) { - return; - } - - // 2. 解析主题,获取 productKey 和 deviceName - String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { - log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); - return; - } - - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - try { - IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); - if (message == null) { - log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); - return; - } - - // 4. 处理业务消息(认证已在连接时完成) - log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", - productKey, deviceName, message.getMethod()); - handleBusinessRequest(message, productKey, deviceName); - } catch (Exception e) { - log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - } - } - - /** - * 在 MQTT 连接时进行设备认证 - * - * @param clientId 客户端 ID - * @param username 用户名 - * @param password 密码 - * @param endpoint MQTT 连接端点 - * @return 认证是否成功 - */ - private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { - try { - // 1. 参数校验 - if (StrUtil.hasEmpty(clientId, username, password)) { - log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - - // 2. 构建认证参数 - IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() - .setClientId(clientId) - .setUsername(username) - .setPassword(password); - - // 3. 调用设备认证 API - CommonResult authResult = deviceApi.authDevice(authParams); - if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { - log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", - clientId, username, authResult.getMsg()); - return false; - } - - // 4. 获取设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); - if (deviceInfo == null) { - log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); - return false; - } - - IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO() - .setProductKey(deviceInfo.getProductKey()) - .setDeviceName(deviceInfo.getDeviceName()); - - CommonResult deviceResult = deviceApi.getDevice(getReqDTO); - if (!deviceResult.isSuccess() || deviceResult.getData() == null) { - log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]", - clientId, username, deviceResult.getMsg()); - return false; - } - - // 5. 注册连接 - IotDeviceRespDTO device = deviceResult.getData(); - registerConnection(endpoint, device, clientId); - - // 6. 发送设备上线消息 - sendOnlineMessage(device); - - return true; - } catch (Exception e) { - log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); - return false; - } - } - - /** - * 处理 register 消息(设备动态注册,使用默认编解码器) - * - * @param clientId 客户端 ID - * @param topic 主题 - * @param payload 消息内容 - * @param endpoint MQTT 连接端点 - */ - private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) { - // 1.1 基础检查 - if (ArrayUtil.isEmpty(payload)) { - return; - } - // 1.2 解析主题,获取 productKey 和 deviceName - String[] topicParts = topic.split("/"); - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { - log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic); - return; - } - String productKey = topicParts[2]; - String deviceName = topicParts[3]; - - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType) - IotDeviceMessage message; - try { - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); - if (message == null) { - log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); - return; - } - } catch (Exception e) { - log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - return; - } - - // 3. 处理设备动态注册请求 - log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]", - productKey, deviceName, message.getMethod()); - try { - handleRegisterRequest(message, productKey, deviceName, endpoint); - } catch (Exception e) { - log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", - clientId, topic, e.getMessage(), e); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要 deviceSecret) - * - * @param message 消息信息 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param endpoint MQTT 连接端点 - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) { - String clientId = endpoint.clientIdentifier(); - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整"); - return; - } - - // 2. 调用动态注册 API - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg()); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData()); - log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]", - params.getDeviceName(), clientId); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常"); - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param endpoint MQTT 连接端点 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param requestId 请求 ID - * @param registerResp 注册响应 - */ - private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, - String requestId, IotDeviceRegisterRespDTO registerResp) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); - - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), - MqttQoS.AT_LEAST_ONCE, false, false); - log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]", - endpoint.clientIdentifier(), e); - } - } - - /** - * 发送注册错误响应 - * - * @param endpoint MQTT 连接端点 - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - */ - private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, - String requestId, String errorMessage) { - try { - // 1. 构建响应消息 - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage); - - // 2. 编码消息(使用默认编解码器,因为设备可能还未注册) - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); - - // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply) - String replyTopic = IotMqttTopicUtils.buildTopicByMethod( - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true); - endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData), - MqttQoS.AT_LEAST_ONCE, false, false); - log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic); - } catch (Exception e) { - log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]", - endpoint.clientIdentifier(), e); - } - } - - /** - * 处理业务请求 - */ - private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { - // 发送消息到消息总线 - message.setServerId(serverId); - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); - } - - /** - * 注册连接 - */ - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) { - IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() - .setDeviceId(device.getId()) - .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setAuthenticated(true) - .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); - } - - /** - * 发送设备上线消息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); - } - } - - /** - * 清理连接 - */ - private void cleanupConnection(MqttEndpoint endpoint) { - try { - IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); - if (connectionInfo != null) { - // 发送设备离线消息 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName()); - } - - // 注销连接 - connectionManager.unregisterConnection(endpoint); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage()); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java index 6eb414ee9..9c8e82787 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java @@ -1,4 +1,4 @@ /** - * 提供设备接入的各种协议的实现 + * 设备接入协议:MQTT、EMQX、HTTP、TCP 等协议的实现 */ -package cn.iocoder.yudao.module.iot.gateway.protocol; \ No newline at end of file +package cn.iocoder.yudao.module.iot.gateway.protocol; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java new file mode 100644 index 000000000..a8c21c871 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConfig.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; + +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT TCP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotTcpConfig { + + /** + * 最大连接数 + */ + @NotNull(message = "最大连接数不能为空") + @Min(value = 1, message = "最大连接数必须大于 0") + private Integer maxConnections = 1000; + /** + * 心跳超时时间(毫秒) + */ + @NotNull(message = "心跳超时时间不能为空") + @Min(value = 1000, message = "心跳超时时间必须大于 1000 毫秒") + private Long keepAliveTimeoutMs = 30000L; + + /** + * 拆包配置 + */ + @Valid + private CodecConfig codec; + + /** + * TCP 拆包配置 + */ + @Data + public static class CodecConfig { + + /** + * 拆包类型 + * + * @see IotTcpCodecTypeEnum + */ + @NotNull(message = "拆包类型不能为空") + private String type; + + /** + * LENGTH_FIELD: 长度字段偏移量 + *

+ * 表示长度字段在消息中的起始位置(从 0 开始) + */ + private Integer lengthFieldOffset; + /** + * LENGTH_FIELD: 长度字段长度(字节数) + *

+ * 常见值:1(最大 255)、2(最大 65535)、4(最大 2GB) + */ + private Integer lengthFieldLength; + /** + * LENGTH_FIELD: 长度调整值 + *

+ * 用于调整长度字段的值,例如长度字段包含头部长度时需要减去头部长度 + */ + private Integer lengthAdjustment = 0; + /** + * LENGTH_FIELD: 跳过的初始字节数 + *

+ * 解码后跳过的字节数,通常等于 lengthFieldOffset + lengthFieldLength + */ + private Integer initialBytesToStrip = 0; + + /** + * DELIMITER: 分隔符 + *

+ * 支持转义字符:\n(换行)、\r(回车)、\r\n(回车换行) + */ + private String delimiter; + + /** + * FIXED_LENGTH: 固定消息长度(字节) + *

+ * 每条消息的固定长度 + */ + private Integer fixedLength; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java deleted file mode 100644 index 3f0cc02bc..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { - - private final IotTcpUpstreamProtocol protocol; - - private final IotDeviceMessageService messageService; - - private final IotTcpConnectionManager connectionManager; - - private final IotMessageBus messageBus; - - private IotTcpDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotTcpDownstreamHandler(messageService, connectionManager); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java new file mode 100644 index 000000000..c8c462ff6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java @@ -0,0 +1,207 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream.IotTcpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream.IotTcpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import io.vertx.core.Vertx; +import io.vertx.core.net.NetServer; +import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT TCP 协议实现 + *

+ * 基于 Vert.x 实现 TCP 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * TCP 服务器 + */ + private NetServer tcpServer; + /** + * TCP 连接管理器 + */ + private final IotTcpConnectionManager connectionManager; + + /** + * 下行消息订阅者 + */ + private IotTcpDownstreamSubscriber downstreamSubscriber; + + /** + * 消息序列化器 + */ + private final IotMessageSerializer serializer; + /** + * TCP 帧编解码器 + */ + private final IotTcpFrameCodec frameCodec; + + public IotTcpProtocol(ProtocolProperties properties) { + IotTcpConfig tcpConfig = properties.getTcp(); + Assert.notNull(tcpConfig, "TCP 协议配置(tcp)不能为空"); + Assert.notNull(tcpConfig.getCodec(), "TCP 拆包配置(tcp.codec)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); + this.serializer = serializerManager.get(serializeType); + // 初始化帧编解码器 + this.frameCodec = IotTcpFrameCodecFactory.create(tcpConfig.getCodec()); + + // 初始化连接管理器 + this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections()); + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.TCP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT TCP 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建服务器选项 + IotTcpConfig tcpConfig = properties.getTcp(); + NetServerOptions options = new NetServerOptions() + .setPort(properties.getPort()) + .setTcpKeepAlive(true) + .setTcpNoDelay(true) + .setReuseAddress(true) + .setIdleTimeout((int) (tcpConfig.getKeepAliveTimeoutMs() / 1000)); // 设置空闲超时 + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + + // 1.3 创建服务器并设置连接处理器 + tcpServer = vertx.createNetServer(options); + tcpServer.connectHandler(socket -> { + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(serverId, frameCodec, serializer, connectionManager); + handler.handle(socket); + }); + + // 1.4 启动 TCP 服务器 + try { + tcpServer.listen().result(); + running = true; + log.info("[start][IoT TCP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer); + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus); + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e); + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 关闭所有连接 + connectionManager.closeAll(); + // 2.2 关闭 TCP 服务器 + if (tcpServer != null) { + try { + tcpServer.close().result(); + log.info("[stop][IoT TCP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} 服务器停止失败]", getId(), e); + } + tcpServer = null; + } + // 2.3 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT TCP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT TCP 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT TCP 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java deleted file mode 100644 index 791c6cbfc..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java +++ /dev/null @@ -1,99 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; - -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.net.NetServer; -import io.vertx.core.net.NetServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 TCP 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotTcpUpstreamProtocol { - - private final IotGatewayProperties.TcpProperties tcpProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotTcpConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private NetServer tcpServer; - - public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotTcpConnectionManager connectionManager, - Vertx vertx) { - this.tcpProperties = tcpProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); - } - - @PostConstruct - public void start() { - // 创建服务器选项 - NetServerOptions options = new NetServerOptions() - .setPort(tcpProperties.getPort()) - .setTcpKeepAlive(true) - .setTcpNoDelay(true) - .setReuseAddress(true); - // 配置 SSL(如果启用) - if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(tcpProperties.getSslKeyPath()) - .setCertPath(tcpProperties.getSslCertPath()); - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - - // 创建服务器并设置连接处理器 - tcpServer = vertx.createNetServer(options); - tcpServer.connectHandler(socket -> { - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, - connectionManager); - handler.handle(socket); - }); - - // 启动服务器 - try { - tcpServer.listen().result(); - log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); - } catch (Exception e) { - log.error("[start][IoT 网关 TCP 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (tcpServer != null) { - try { - tcpServer.close().result(); - log.info("[stop][IoT 网关 TCP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 TCP 协议停止失败]", e); - } - } - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java new file mode 100644 index 000000000..344001f56 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpCodecTypeEnum.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter.IotTcpDelimiterFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpFixedLengthFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length.IotTcpLengthFieldFrameCodec; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * IoT TCP 拆包类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum IotTcpCodecTypeEnum { + + /** + * 基于固定长度的拆包 + */ + FIXED_LENGTH("fixed_length", IotTcpFixedLengthFrameCodec.class), + + /** + * 基于分隔符的拆包 + */ + DELIMITER("delimiter", IotTcpDelimiterFrameCodec.class), + + /** + * 基于长度字段的拆包 + */ + LENGTH_FIELD("length_field", IotTcpLengthFieldFrameCodec.class), + ; + + /** + * 类型标识 + */ + private final String type; + /** + * 编解码器类 + */ + private final Class codecClass; + + /** + * 根据类型获取枚举 + * + * @param type 类型标识 + * @return 枚举值 + */ + public static IotTcpCodecTypeEnum of(String type) { + return ArrayUtil.firstMatch(e -> e.getType().equalsIgnoreCase(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java new file mode 100644 index 000000000..d002d2043 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodec.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; + +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; + +/** + * IoT TCP 帧编解码器接口 + *

+ * 用于解决 TCP 粘包/拆包问题,提供解码(拆包)和编码(加帧)能力 + * + * @author 芋道源码 + */ +public interface IotTcpFrameCodec { + + /** + * 获取编解码器类型 + * + * @return 编解码器类型 + */ + IotTcpCodecTypeEnum getType(); + + /** + * 创建解码器(RecordParser) + *

+ * 每个连接调用一次,返回的 parser 需绑定到 socket.handler() + * + * @param handler 消息处理器,当收到完整的消息帧后回调 + * @return RecordParser 实例 + */ + RecordParser createDecodeParser(Handler handler); + + /** + * 编码消息(加帧) + *

+ * 根据不同的编解码类型添加帧头/分隔符 + * + * @param data 原始数据 + * @return 编码后的数据(带帧头/分隔符) + */ + Buffer encode(byte[] data); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java new file mode 100644 index 000000000..a40783ac6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/IotTcpFrameCodecFactory.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; + +/** + * IoT TCP 帧编解码器工厂 + * + * @author 芋道源码 + */ +public class IotTcpFrameCodecFactory { + + /** + * 根据配置创建编解码器 + * + * @param config 拆包配置 + * @return 编解码器实例,如果配置为空则返回 null + */ + public static IotTcpFrameCodec create(IotTcpConfig.CodecConfig config) { + Assert.notNull(config, "CodecConfig 不能为空"); + IotTcpCodecTypeEnum type = IotTcpCodecTypeEnum.of(config.getType()); + Assert.notNull(type, "不支持的 CodecType 类型:" + config.getType()); + return ReflectUtil.newInstance(type.getCodecClass(), config); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java new file mode 100644 index 000000000..85e75cc3c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.delimiter; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT TCP 分隔符帧编解码器 + *

+ * 基于分隔符的拆包策略,消息格式:消息内容 + 分隔符 + *

+ * 支持的分隔符: + *

    + *
  • \n - 换行符
  • + *
  • \r - 回车符
  • + *
  • \r\n - 回车换行
  • + *
  • 自定义字符串
  • + *
+ * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec { + + /** + * 最大记录大小(64KB),防止 DoS 攻击 + */ + private static final int MAX_RECORD_SIZE = 65536; + + /** + * 解析后的分隔符字节数组 + */ + private final byte[] delimiterBytes; + + public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空"); + this.delimiterBytes = parseDelimiter(config.getDelimiter()); + } + + @Override + public IotTcpCodecTypeEnum getType() { + return IotTcpCodecTypeEnum.DELIMITER; + } + + @Override + public RecordParser createDecodeParser(Handler handler) { + RecordParser parser = RecordParser.newDelimited(Buffer.buffer(delimiterBytes)); + parser.maxRecordSize(MAX_RECORD_SIZE); // 设置最大记录大小,防止 DoS 攻击 + // 处理完整消息(不包含分隔符) + parser.handler(handler); + parser.exceptionHandler(ex -> { + throw new RuntimeException("[createDecodeParser][解析异常]", ex); + }); + return parser; + } + + @Override + public Buffer encode(byte[] data) { + Buffer buffer = Buffer.buffer(); + buffer.appendBytes(data); + buffer.appendBytes(delimiterBytes); + return buffer; + } + + /** + * 解析分隔符字符串为字节数组 + *

+ * 支持转义字符:\n、\r、\r\n、\t + * + * @param delimiter 分隔符字符串 + * @return 分隔符字节数组 + */ + private byte[] parseDelimiter(String delimiter) { + // 处理转义字符 + String parsed = delimiter + .replace("\\r\\n", "\r\n") + .replace("\\r", "\r") + .replace("\\n", "\n") + .replace("\\t", "\t"); + return StrUtil.utf8Bytes(parsed); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java new file mode 100644 index 000000000..6898e9674 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT TCP 定长帧编解码器 + *

+ * 基于固定长度的拆包策略,每条消息固定字节数 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpFixedLengthFrameCodec implements IotTcpFrameCodec { + + /** + * 固定消息长度 + */ + private final int fixedLength; + + public IotTcpFixedLengthFrameCodec(IotTcpConfig.CodecConfig config) { + Assert.notNull(config.getFixedLength(), "fixedLength 不能为空"); + this.fixedLength = config.getFixedLength(); + } + + @Override + public IotTcpCodecTypeEnum getType() { + return IotTcpCodecTypeEnum.FIXED_LENGTH; + } + + @Override + public RecordParser createDecodeParser(Handler handler) { + RecordParser parser = RecordParser.newFixed(fixedLength); + parser.handler(handler); + parser.exceptionHandler(ex -> { + throw new RuntimeException("[createDecodeParser][解析异常]", ex); + }); + return parser; + } + + @Override + public Buffer encode(byte[] data) { + // 校验数据长度不能超过固定长度 + if (data.length > fixedLength) { + throw new IllegalArgumentException(String.format( + "数据长度 %d 超过固定长度 %d", data.length, fixedLength)); + } + Buffer buffer = Buffer.buffer(fixedLength); + buffer.appendBytes(data); + // 如果数据不足固定长度,填充 0(RecordParser.newFixed 解码时按固定长度读取,所以发送端需要填充) + if (data.length < fixedLength) { + byte[] padding = new byte[fixedLength - data.length]; + buffer.appendBytes(padding); + } + return buffer; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java new file mode 100644 index 000000000..52f84eefa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java @@ -0,0 +1,181 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.length; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * IoT TCP 长度字段帧编解码器 + *

+ * 基于长度字段的拆包策略,消息格式:[长度字段][消息体] + *

+ * 参数说明: + *

    + *
  • lengthFieldOffset: 长度字段在消息中的偏移量
  • + *
  • lengthFieldLength: 长度字段的字节数(1/2/4)
  • + *
  • lengthAdjustment: 长度调整值,用于调整长度字段的实际含义
  • + *
  • initialBytesToStrip: 解码后跳过的字节数
  • + *
+ * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpLengthFieldFrameCodec implements IotTcpFrameCodec { + + /** + * 最大帧长度(64KB),防止 DoS 攻击 + */ + private static final int MAX_FRAME_LENGTH = 65536; + + private final int lengthFieldOffset; + private final int lengthFieldLength; + private final int lengthAdjustment; + private final int initialBytesToStrip; + + /** + * 头部长度 = 长度字段偏移量 + 长度字段长度 + */ + private final int headerLength; + + public IotTcpLengthFieldFrameCodec(IotTcpConfig.CodecConfig config) { + Assert.notNull(config.getLengthFieldOffset(), "lengthFieldOffset 不能为空"); + Assert.notNull(config.getLengthFieldLength(), "lengthFieldLength 不能为空"); + Assert.notNull(config.getLengthAdjustment(), "lengthAdjustment 不能为空"); + Assert.notNull(config.getInitialBytesToStrip(), "initialBytesToStrip 不能为空"); + this.lengthFieldOffset = config.getLengthFieldOffset(); + this.lengthFieldLength = config.getLengthFieldLength(); + this.lengthAdjustment = config.getLengthAdjustment(); + this.initialBytesToStrip = config.getInitialBytesToStrip(); + this.headerLength = lengthFieldOffset + lengthFieldLength; + } + + @Override + public IotTcpCodecTypeEnum getType() { + return IotTcpCodecTypeEnum.LENGTH_FIELD; + } + + @Override + public RecordParser createDecodeParser(Handler handler) { + // 创建状态机:先读取头部,再读取消息体 + RecordParser parser = RecordParser.newFixed(headerLength); + parser.maxRecordSize(MAX_FRAME_LENGTH); // 设置最大记录大小,防止 DoS 攻击 + final AtomicReference bodyLength = new AtomicReference<>(null); // 消息体长度,null 表示读取头部阶段 + final AtomicReference headerBuffer = new AtomicReference<>(null); // 头部消息 + + // 处理读取到的数据 + parser.handler(buffer -> { + if (bodyLength.get() == null) { + // 阶段 1: 读取头部,解析长度字段 + headerBuffer.set(buffer.copy()); + int length = readLength(buffer, lengthFieldOffset, lengthFieldLength); + int frameBodyLength = length + lengthAdjustment; + // 检查帧长度是否合法 + if (frameBodyLength < 0) { + throw new IllegalStateException(String.format( + "[createDecodeParser][帧长度异常,length: %d, frameBodyLength: %d]", + length, frameBodyLength)); + } + // 消息体为空,抛出异常 + if (frameBodyLength == 0) { + throw new IllegalStateException("[createDecodeParser][消息体不能为空]"); + } + + // 【重要】切换到读取消息体模式 + bodyLength.set(frameBodyLength); + parser.fixedSizeMode(frameBodyLength); + } else { + // 阶段 2: 读取消息体,组装完整帧 + Buffer frame = processFrame(headerBuffer.get(), buffer); + // 重置状态,准备读取下一帧 + bodyLength.set(null); + headerBuffer.set(null); + parser.fixedSizeMode(headerLength); + + // 【重要】处理完整消息 + handler.handle(frame); + } + }); + parser.exceptionHandler(ex -> { + throw new RuntimeException("[createDecodeParser][解析异常]", ex); + }); + return parser; + } + + @Override + public Buffer encode(byte[] data) { + Buffer buffer = Buffer.buffer(); + // 计算要写入的长度值 + int lengthValue = data.length - lengthAdjustment; + // 写入偏移量前的填充字节(如果有) + for (int i = 0; i < lengthFieldOffset; i++) { + buffer.appendByte((byte) 0); + } + // 写入长度字段 + writeLength(buffer, lengthValue, lengthFieldLength); + // 写入消息体 + buffer.appendBytes(data); + return buffer; + } + + /** + * 从 Buffer 中读取长度字段 + */ + @SuppressWarnings("EnhancedSwitchMigration") + private int readLength(Buffer buffer, int offset, int length) { + switch (length) { + case 1: + return buffer.getUnsignedByte(offset); + case 2: + return buffer.getUnsignedShort(offset); + case 4: + return buffer.getInt(offset); + default: + throw new IllegalArgumentException("不支持的长度字段长度: " + length); + } + } + + /** + * 向 Buffer 中写入长度字段 + */ + private void writeLength(Buffer buffer, int length, int fieldLength) { + switch (fieldLength) { + case 1: + buffer.appendByte((byte) length); + break; + case 2: + buffer.appendShort((short) length); + break; + case 4: + buffer.appendInt(length); + break; + default: + throw new IllegalArgumentException("不支持的长度字段长度: " + fieldLength); + } + } + + /** + * 处理帧数据(根据 initialBytesToStrip 跳过指定字节) + */ + private Buffer processFrame(Buffer header, Buffer body) { + Buffer fullFrame = Buffer.buffer(); + if (header != null) { + fullFrame.appendBuffer(header); + } + if (body != null) { + fullFrame.appendBuffer(body); + } + // 根据 initialBytesToStrip 跳过指定字节 + if (initialBytesToStrip > 0 && initialBytesToStrip < fullFrame.length()) { + return fullFrame.slice(initialBytesToStrip, fullFrame.length()); + } + return fullFrame; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java new file mode 100644 index 000000000..933d56ade --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import io.vertx.core.buffer.Buffer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 TCP 下行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotTcpDownstreamHandler { + + private final IotTcpConnectionManager connectionManager; + + /** + * TCP 帧编解码器(处理粘包/拆包) + */ + private final IotTcpFrameCodec codec; + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + + /** + * 处理下行消息 + */ + public void handle(IotDeviceMessage message) { + try { + // 1.1 检查是否是属性设置消息 + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) { + return; + } + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { + log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod()); + return; + } + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + // 1.2 检查设备连接 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( + message.getDeviceId()); + if (connectionInfo == null) { + log.warn("[handle][连接信息不存在,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + return; + } + + // 2. 序列化 + 帧编码 + byte[] payload = serializer.serialize(message); + Buffer frameData = codec.encode(payload); + + // 3. 发送到设备 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), frameData.getBytes()); + if (!success) { + throw new RuntimeException("下行消息发送失败"); + } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), frameData.length()); + } catch (Exception e) { + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", + message.getDeviceId(), message.getMethod(), message, e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java new file mode 100644 index 000000000..0344193de --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { + + private final IotTcpDownstreamHandler downstreamHandler; + + public IotTcpDownstreamSubscriber(IotProtocol protocol, + IotTcpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java new file mode 100644 index 000000000..845568e3b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java @@ -0,0 +1,328 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetSocket; +import io.vertx.core.parsetools.RecordParser; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * TCP 上行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpUpstreamHandler implements Handler { + + private static final String AUTH_METHOD = "auth"; + + private final String serverId; + + /** + * TCP 帧编解码器(处理粘包/拆包) + */ + private final IotTcpFrameCodec codec; + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + /** + * TCP 连接管理器 + */ + private final IotTcpConnectionManager connectionManager; + + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + private final IotDeviceCommonApi deviceApi; + + public IotTcpUpstreamHandler(String serverId, + IotTcpFrameCodec codec, + IotMessageSerializer serializer, + IotTcpConnectionManager connectionManager) { + Assert.notNull(codec, "TCP FrameCodec 必须配置"); + Assert.notNull(serializer, "消息序列化器必须配置"); + Assert.notNull(connectionManager, "连接管理器不能为空"); + this.serverId = serverId; + this.codec = codec; + this.serializer = serializer; + this.connectionManager = connectionManager; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + public void handle(NetSocket socket) { + String remoteAddress = String.valueOf(socket.remoteAddress()); + log.debug("[handle][设备连接,地址: {}]", remoteAddress); + + // 1. 设置异常和关闭处理器 + socket.exceptionHandler(ex -> { + log.warn("[handle][连接异常,地址: {}]", remoteAddress, ex); + socket.close(); + }); + socket.closeHandler(v -> { + log.debug("[handle][连接关闭,地址: {}]", remoteAddress); + cleanupConnection(socket); + }); + + // 2.1 设置消息处理器 + Handler messageHandler = buffer -> { + try { + processMessage(buffer, socket); + } catch (Exception e) { + log.error("[handle][消息处理失败,地址: {}]", remoteAddress, e); + socket.close(); + } + }; + // 2.2 使用拆包器处理粘包/拆包 + RecordParser parser = codec.createDecodeParser(messageHandler); + socket.handler(parser); + log.debug("[handle][启用 {} 拆包器,地址: {}]", codec.getType(), remoteAddress); + } + + /** + * 处理消息 + * + * @param buffer 消息 + * @param socket 网络连接 + */ + private void processMessage(Buffer buffer, NetSocket socket) { + IotDeviceMessage message = null; + try { + // 1. 反序列化消息 + message = serializer.deserialize(buffer.getBytes()); + if (message == null) { + sendErrorResponse(socket, null, null, BAD_REQUEST.getCode(), "消息反序列化失败"); + return; + } + + // 2. 根据消息类型路由处理 + if (AUTH_METHOD.equals(message.getMethod())) { + // 认证请求 + handleAuthenticationRequest(message, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + // 设备动态注册请求 + handleRegisterRequest(message, socket); + } else { + // 业务消息 + handleBusinessRequest(message, socket); + } + } catch (ServiceException e) { + // 业务异常,返回对应的错误码和错误信息 + log.warn("[processMessage][业务异常,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + // 参数校验失败,返回 400 + log.warn("[processMessage][参数校验失败,地址: {},错误: {}]", socket.remoteAddress(), e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + // 其他异常,返回 500,并重新抛出让上层关闭连接 + log.error("[processMessage][处理消息失败,地址: {}]", socket.remoteAddress(), e); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + throw e; + } + } + + /** + * 处理认证请求 + * + * @param message 消息信息 + * @param socket 网络连接 + */ + @SuppressWarnings("DuplicatedCode") + private void handleAuthenticationRequest(IotDeviceMessage message, NetSocket socket) { + // 1. 解析认证参数 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); + Assert.notNull(authParams, "认证参数不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); + + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + authResult.checkError(); + if (BooleanUtil.isFalse(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3.1 注册连接 + registerConnection(socket, device); + // 3.2 发送上线消息 + sendOnlineMessage(device); + // 3.3 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), AUTH_METHOD, "认证成功"); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName()); + } + + /** + * 处理设备动态注册请求(一型一密,不需要认证) + * + * @param message 消息信息 + * @param socket 网络连接 + * @see 阿里云 - 一型一密 + */ + @SuppressWarnings("DuplicatedCode") + private void handleRegisterRequest(IotDeviceMessage message, NetSocket socket) { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); + log.info("[handleRegisterRequest][注册成功,地址: {},设备名: {}]", + socket.remoteAddress(), params.getDeviceName()); + } + + /** + * 处理业务请求 + * + * @param message 消息信息 + * @param socket 网络连接 + */ + private void handleBusinessRequest(IotDeviceMessage message, NetSocket socket) { + // 1. 获取认证信息并处理业务消息 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo == null) { + log.error("[handleBusinessRequest][无法获取连接信息,地址: {}]", socket.remoteAddress()); + sendErrorResponse(socket, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息"); + return; + } + + // 2. 发送消息到消息总线 + deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + log.info("[handleBusinessRequest][发送消息到消息总线,地址: {},消息: {}]", socket.remoteAddress(), message); + } + + /** + * 注册连接信息 + * + * @param socket 网络连接 + * @param device 设备 + */ + private void registerConnection(NetSocket socket, IotDeviceRespDTO device) { + IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()); + connectionManager.registerConnection(socket, device.getId(), connectionInfo); + } + + /** + * 发送设备上线消息 + * + * @param device 设备信息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + } + + /** + * 清理连接 + * + * @param socket 网络连接 + */ + private void cleanupConnection(NetSocket socket) { + // 1. 发送离线消息 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + } + + // 2. 注销连接 + connectionManager.unregisterConnection(socket); + } + + // ===================== 发送响应消息 ===================== + + /** + * 发送成功响应 + * + * @param socket 网络连接 + * @param requestId 请求 ID + * @param method 方法名 + * @param data 响应数据 + */ + private void sendSuccessResponse(NetSocket socket, String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); + writeResponse(socket, responseMessage); + } + + /** + * 发送错误响应 + * + * @param socket 网络连接 + * @param requestId 请求 ID + * @param method 方法名 + * @param code 错误码 + * @param msg 错误消息 + */ + private void sendErrorResponse(NetSocket socket, String requestId, String method, Integer code, String msg) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); + writeResponse(socket, responseMessage); + } + + /** + * 写入响应到 Socket + * + * @param socket 网络连接 + * @param responseMessage 响应消息 + */ + private void writeResponse(NetSocket socket, IotDeviceMessage responseMessage) { + byte[] serializedData = serializer.serialize(responseMessage); + Buffer frameData = codec.encode(serializedData); + socket.write(frameData); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java index c0f2cf7aa..20065f1b4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java @@ -4,8 +4,9 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -20,9 +21,13 @@ import java.util.concurrent.ConcurrentHashMap; * @author 芋道源码 */ @Slf4j -@Component public class IotTcpConnectionManager { + /** + * 最大连接数 + */ + private final int maxConnections; + /** * 连接信息映射:NetSocket -> 连接信息 */ @@ -33,6 +38,10 @@ public class IotTcpConnectionManager { */ private final Map deviceSocketMap = new ConcurrentHashMap<>(); + public IotTcpConnectionManager(int maxConnections) { + this.maxConnections = maxConnections; + } + /** * 注册设备连接(包含认证信息) * @@ -40,15 +49,19 @@ public class IotTcpConnectionManager { * @param deviceId 设备 ID * @param connectionInfo 连接信息 */ - public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { + public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { + // 检查连接数是否已达上限(同步方法确保检查和注册的原子性) + if (connectionMap.size() >= maxConnections) { + throw new IllegalStateException("连接数已达上限: " + maxConnections); + } // 如果设备已有其他连接,先清理旧连接 NetSocket oldSocket = deviceSocketMap.get(deviceId); if (oldSocket != null && oldSocket != socket) { log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", deviceId, oldSocket.remoteAddress()); - oldSocket.close(); - // 清理旧连接的映射 + // 先清理映射,再关闭连接 connectionMap.remove(oldSocket); + oldSocket.close(); } // 注册新连接 @@ -69,25 +82,11 @@ public class IotTcpConnectionManager { return; } Long deviceId = connectionInfo.getDeviceId(); - deviceSocketMap.remove(deviceId); + // 仅当 deviceSocketMap 中的 socket 是当前 socket 时才移除,避免误删新连接 + deviceSocketMap.remove(deviceId, socket); log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); } - /** - * 检查连接是否已认证 - */ - public boolean isAuthenticated(NetSocket socket) { - ConnectionInfo info = connectionMap.get(socket); - return info != null; - } - - /** - * 检查连接是否未认证 - */ - public boolean isNotAuthenticated(NetSocket socket) { - return !isAuthenticated(socket); - } - /** * 获取连接信息 */ @@ -125,6 +124,24 @@ public class IotTcpConnectionManager { } } + /** + * 关闭所有连接 + */ + public void closeAll() { + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 + List sockets = new ArrayList<>(connectionMap.keySet()); + connectionMap.clear(); + deviceSocketMap.clear(); + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) + for (NetSocket socket : sockets) { + try { + socket.close(); + } catch (Exception ignored) { + // 连接可能已关闭,忽略异常 + } + } + } + /** * 连接信息(包含认证信息) */ @@ -144,15 +161,6 @@ public class IotTcpConnectionManager { */ private String deviceName; - /** - * 客户端 ID - */ - private String clientId; - /** - * 消息编解码类型(认证后确定) - */ - private String codecType; - } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java deleted file mode 100644 index 374e75287..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 TCP 下行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotTcpDownstreamHandler { - - private final IotDeviceMessageService deviceMessageService; - - private final IotTcpConnectionManager connectionManager; - - /** - * 处理下行消息 - */ - public void handle(IotDeviceMessage message) { - try { - log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); - - // 1. 获取连接信息(包含 codecType) - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( - message.getDeviceId()); - if (connectionInfo == null) { - log.error("[handle][连接信息不存在,设备 ID: {}]", message.getDeviceId()); - return; - } - - // 2. 使用连接时的 codecType 编码消息,并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType()); - boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes); - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); - } - } catch (Exception e) { - log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", - message.getDeviceId(), message.getMethod(), message, e); - } - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java deleted file mode 100644 index 4a20f46af..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ /dev/null @@ -1,508 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.NetSocket; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -/** - * TCP 上行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotTcpUpstreamHandler implements Handler { - - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; - - private static final String AUTH_METHOD = "auth"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotTcpConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotTcpConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.connectionManager = connectionManager; - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - } - - @Override - public void handle(NetSocket socket) { - String clientId = IdUtil.simpleUUID(); - log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - - // 设置异常和关闭处理器 - socket.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - socket.closeHandler(v -> { - log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - - // 设置消息处理器 - socket.handler(buffer -> { - // TODO @AI:TODO @芋艿:这里应该有拆粘包的问题; - try { - processMessage(clientId, buffer, socket); - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); - cleanupConnection(socket); - socket.close(); - } - }); - } - - /** - * 处理消息 - * - * @param clientId 客户端 ID - * @param buffer 消息 - * @param socket 网络连接 - * @throws Exception 消息解码失败时抛出异常 - */ - private void processMessage(String clientId, Buffer buffer, NetSocket socket) throws Exception { - // 1. 基础检查 - if (buffer == null || buffer.length() == 0) { - return; - } - - // 2. 获取消息格式类型 - String codecType = getMessageCodecType(buffer, socket); - - // 3. 解码消息 - IotDeviceMessage message; - try { - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); - if (message == null) { - throw new Exception("解码后消息为空"); - } - } catch (Exception e) { - // 消息格式错误时抛出异常,由上层处理连接断开 - throw new Exception("消息解码失败: " + e.getMessage(), e); - } - - // 4. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(message.getMethod())) { - // 认证请求 - handleAuthenticationRequest(clientId, message, codecType, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(clientId, message, codecType, socket); - } else { - // 业务消息 - handleBusinessRequest(clientId, message, codecType, socket); - } - } catch (Exception e) { - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", - clientId, message.getMethod(), e); - // 发送错误响应,避免客户端一直等待 - try { - sendErrorResponse(socket, message.getRequestId(), "消息处理失败", codecType); - } catch (Exception responseEx) { - log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); - } - } - } - - /** - * 处理认证请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param socket 网络连接 - */ - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, String codecType, - NetSocket socket) { - try { - // 1.1 解析认证参数 - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整", codecType); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", - clientId, authParams.getUsername()); - sendErrorResponse(socket, message.getRequestId(), "认证失败", codecType); - return; - } - - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3.1 注册连接 - registerConnection(socket, device, clientId, codecType); - // 3.2 发送上线消息 - sendOnlineMessage(device); - // 3.3 发送成功响应 - sendSuccessResponse(socket, message.getRequestId(), "认证成功", codecType); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", - device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "认证处理异常", codecType); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要认证) - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param socket 网络连接 - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(String clientId, IotDeviceMessage message, String codecType, - NetSocket socket) { - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整", codecType); - return; - } - - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(socket, message.getRequestId(), result.getMsg(), codecType); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData(), codecType); - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", - clientId, params.getDeviceName()); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "注册处理异常", codecType); - } - } - - /** - * 处理业务请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param socket 网络连接 - */ - private void handleBusinessRequest(String clientId, IotDeviceMessage message, String codecType, NetSocket socket) { - try { - // 1. 检查认证状态 - if (connectionManager.isNotAuthenticated(socket)) { - log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "请先进行认证", codecType); - return; - } - - // 2. 获取认证信息并处理业务消息 - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - - // 3. 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", - clientId, message.toString()); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); - } - } - - /** - * 获取消息编解码类型 - * - * @param buffer 消息 - * @param socket 网络连接 - * @return 消息编解码类型 - */ - private String getMessageCodecType(Buffer buffer, NetSocket socket) { - // 1. 如果已认证,优先使用缓存的编解码类型 - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo != null - && StrUtil.isNotBlank(connectionInfo.getCodecType())) { - return connectionInfo.getCodecType(); - } - - // 2. 未认证时检测消息格式类型 - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY - : CODEC_TYPE_JSON; - } - - /** - * 注册连接信息 - * - * @param socket 网络连接 - * @param device 设备 - * @param clientId 客户端 ID - * @param codecType 消息编解码类型 - */ - private void registerConnection(NetSocket socket, IotDeviceRespDTO device, - String clientId, String codecType) { - IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() - .setDeviceId(device.getId()) - .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setCodecType(codecType); - // 注册连接 - connectionManager.registerConnection(socket, device.getId(), connectionInfo); - } - - /** - * 发送设备上线消息 - * - * @param device 设备信息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } - } - - /** - * 清理连接 - * - * @param socket 网络连接 - */ - private void cleanupConnection(NetSocket socket) { - try { - // 1. 发送离线消息(如果已认证) - IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo != null) { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - } - - // 2. 注销连接 - connectionManager.unregisterConnection(socket); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败]", e); - } - } - - /** - * 发送响应消息 - * - * @param socket 网络连接 - * @param success 是否成功 - * @param message 消息 - * @param requestId 请求 ID - * @param codecType 消息编解码类型 - */ - private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { - try { - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, - code, message); - - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.write(Buffer.buffer(encodedData)); - - } catch (Exception e) { - log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); - } - } - - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送错误响应 - * - * @param socket 网络连接 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - * @param codecType 消息编解码类型 - */ - private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage, String codecType) { - sendResponse(socket, false, errorMessage, requestId, codecType); - } - - /** - * 发送成功响应 - * - * @param socket 网络连接 - * @param requestId 请求 ID - * @param message 消息 - * @param codecType 消息编解码类型 - */ - @SuppressWarnings("SameParameterValue") - private void sendSuccessResponse(NetSocket socket, String requestId, String message, String codecType) { - sendResponse(socket, true, message, requestId, codecType); - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - String jsonStr = JsonUtils.toJsonString(params); - return JsonUtils.parseObject(jsonStr, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket 网络连接 - * @param requestId 请求 ID - * @param registerResp 注册响应 - * @param codecType 消息编解码类型 - */ - private void sendRegisterSuccessResponse(NetSocket socket, String requestId, - IotDeviceRegisterRespDTO registerResp, String codecType) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.write(Buffer.buffer(encodedData)); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); - } - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java new file mode 100644 index 000000000..95a6291e4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpConfig.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT UDP 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotUdpConfig { + + /** + * 最大会话数 + */ + @NotNull(message = "最大会话数不能为空") + @Min(value = 1, message = "最大会话数必须大于 0") + private Integer maxSessions = 1000; + /** + * 会话超时时间(毫秒) + *

+ * 基于 Guava Cache 的 expireAfterAccess 实现自动过期清理 + */ + @NotNull(message = "会话超时时间不能为空") + @Min(value = 1000, message = "会话超时时间必须大于 1000 毫秒") + private Long sessionTimeoutMs = 60000L; + + /** + * 接收缓冲区大小(字节) + */ + @NotNull(message = "接收缓冲区大小不能为空") + @Min(value = 1024, message = "接收缓冲区大小必须大于 1024 字节") + private Integer receiveBufferSize = 65536; + /** + * 发送缓冲区大小(字节) + */ + @NotNull(message = "发送缓冲区大小不能为空") + @Min(value = 1024, message = "发送缓冲区大小必须大于 1024 字节") + private Integer sendBufferSize = 65536; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java deleted file mode 100644 index 87f878551..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 UDP 下游订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotUdpDownstreamSubscriber implements IotMessageSubscriber { - - private final IotUdpUpstreamProtocol protocol; - - private final IotDeviceMessageService messageService; - - private final IotUdpSessionManager sessionManager; - - private final IotMessageBus messageBus; - - private IotUdpDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotUdpDownstreamHandler(messageService, sessionManager, protocol); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][UDP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java new file mode 100644 index 000000000..d628c13b4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java @@ -0,0 +1,188 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream.IotUdpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream.IotUdpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import io.vertx.core.Vertx; +import io.vertx.core.datagram.DatagramSocket; +import io.vertx.core.datagram.DatagramSocketOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT UDP 协议实现 + *

+ * 基于 Vert.x 实现 UDP 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotUdpProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * UDP 服务器 + */ + @Getter + private DatagramSocket udpSocket; + /** + * UDP 会话管理器 + */ + private final IotUdpSessionManager sessionManager; + + /** + * 下行消息订阅者 + */ + private IotUdpDownstreamSubscriber downstreamSubscriber; + + /** + * 消息序列化器 + */ + private final IotMessageSerializer serializer; + + public IotUdpProtocol(ProtocolProperties properties) { + IotUdpConfig udpConfig = properties.getUdp(); + Assert.notNull(udpConfig, "UDP 协议配置(udp)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); + this.serializer = serializerManager.get(serializeType); + + // 初始化会话管理器 + this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions(), udpConfig.getSessionTimeoutMs()); + + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.UDP; + } + + @Override + public void start() { + if (running) { + log.warn("[start][IoT UDP 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 和 下行消息订阅者 + this.vertx = Vertx.vertx(); + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer); + this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus); + + // 1.2 创建 UDP Socket 选项 + IotUdpConfig udpConfig = properties.getUdp(); + DatagramSocketOptions options = new DatagramSocketOptions() + .setReceiveBufferSize(udpConfig.getReceiveBufferSize()) + .setSendBufferSize(udpConfig.getSendBufferSize()) + .setReuseAddress(true); + + // 1.3 创建 UDP Socket + udpSocket = vertx.createDatagramSocket(options); + + // 1.4 创建上行消息处理器 + IotUdpUpstreamHandler upstreamHandler = new IotUdpUpstreamHandler(serverId, sessionManager, serializer); + + // 1.5 启动 UDP 服务器(阻塞式) + try { + udpSocket.listen(properties.getPort(), "0.0.0.0").result(); + // 设置数据包处理器 + udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); + running = true; + log.info("[start][IoT UDP 协议 {} 启动成功,端口:{},serverId:{}]", + getId(), properties.getPort(), serverId); + + // 2. 启动下行消息订阅者 + this.downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), e); + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 关闭 UDP Socket + if (udpSocket != null) { + try { + udpSocket.close().result(); + log.info("[stop][IoT UDP 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} 服务器停止失败]", getId(), e); + } + udpSocket = null; + } + // 2.2 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT UDP 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT UDP 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT UDP 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java deleted file mode 100644 index 744868389..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java +++ /dev/null @@ -1,171 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.router.IotUdpUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.datagram.DatagramSocket; -import io.vertx.core.datagram.DatagramSocketOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -/** - * IoT 网关 UDP 协议:接收设备上行消息 - *

- * 采用 Vertx DatagramSocket 实现 UDP 服务器,主要功能: - * 1. 监听 UDP 端口,接收设备消息 - * 2. 定期清理不活跃的设备地址映射 - * 3. 提供 UDP Socket 用于下行消息发送 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpUpstreamProtocol { - - private final IotGatewayProperties.UdpProperties udpProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotUdpSessionManager sessionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - @Getter - private DatagramSocket udpSocket; - - /** - * 会话清理定时器 ID - */ - private Long cleanTimerId; - - private IotUdpUpstreamHandler upstreamHandler; - - public IotUdpUpstreamProtocol(IotGatewayProperties.UdpProperties udpProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotUdpSessionManager sessionManager, - Vertx vertx) { - this.udpProperties = udpProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.sessionManager = sessionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(udpProperties.getPort()); - } - - @PostConstruct - public void start() { - // 1. 初始化上行消息处理器 - this.upstreamHandler = new IotUdpUpstreamHandler(this, messageService, deviceService, sessionManager); - - // 2. 创建 UDP Socket 选项 - DatagramSocketOptions options = new DatagramSocketOptions() - .setReceiveBufferSize(udpProperties.getReceiveBufferSize()) - .setSendBufferSize(udpProperties.getSendBufferSize()) - .setReuseAddress(true); - - // 3. 创建 UDP Socket - udpSocket = vertx.createDatagramSocket(options); - - // 4. 监听端口 - udpSocket.listen(udpProperties.getPort(), "0.0.0.0", result -> { - if (result.failed()) { - log.error("[start][IoT 网关 UDP 协议启动失败]", result.cause()); - return; - } - // 设置数据包处理器 - udpSocket.handler(packet -> upstreamHandler.handle(packet, udpSocket)); - log.info("[start][IoT 网关 UDP 协议启动成功,端口:{},接收缓冲区:{} 字节,发送缓冲区:{} 字节]", - udpProperties.getPort(), udpProperties.getReceiveBufferSize(), - udpProperties.getSendBufferSize()); - - // 5. 启动会话清理定时器 - startSessionCleanTimer(); - }); - } - - @PreDestroy - public void stop() { - // 1. 取消会话清理定时器 - if (cleanTimerId != null) { - vertx.cancelTimer(cleanTimerId); - cleanTimerId = null; - log.info("[stop][会话清理定时器已取消]"); - } - - // 2. 关闭 UDP Socket - if (udpSocket != null) { - try { - udpSocket.close().result(); - log.info("[stop][IoT 网关 UDP 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 UDP 协议停止失败]", e); - } - } - } - - /** - * 启动会话清理定时器 - */ - private void startSessionCleanTimer() { - cleanTimerId = vertx.setPeriodic(udpProperties.getSessionCleanIntervalMs(), id -> { - try { - // 1. 清理超时的设备地址映射,并获取离线设备列表 - List offlineDeviceIds = sessionManager.cleanExpiredMappings(udpProperties.getSessionTimeoutMs()); - - // 2. 为每个离线设备发送离线消息 - for (Long deviceId : offlineDeviceIds) { - sendOfflineMessage(deviceId); - } - if (CollUtil.isNotEmpty(offlineDeviceIds)) { - log.info("[cleanExpiredMappings][本次清理 {} 个超时设备]", offlineDeviceIds.size()); - } - } catch (Exception e) { - log.error("[cleanExpiredMappings][清理超时会话失败]", e); - } - }); - log.info("[startSessionCleanTimer][会话清理定时器启动,间隔:{} ms,超时:{} ms]", - udpProperties.getSessionCleanIntervalMs(), udpProperties.getSessionTimeoutMs()); - } - - /** - * 发送设备离线消息 - * - * @param deviceId 设备 ID - */ - private void sendOfflineMessage(Long deviceId) { - try { - // 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceId); - if (device == null) { - log.warn("[sendOfflineMessage][设备不存在,设备 ID: {}]", deviceId); - return; - } - - // 发送离线消息 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - messageService.sendDeviceMessage(offlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - log.info("[sendOfflineMessage][发送离线消息,设备 ID: {},设备名: {}]", - deviceId, device.getDeviceName()); - } catch (Exception e) { - log.error("[sendOfflineMessage][发送离线消息失败,设备 ID: {}]", deviceId, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java new file mode 100644 index 000000000..6caf71abe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamHandler.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import io.vertx.core.datagram.DatagramSocket; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 UDP 下行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotUdpDownstreamHandler { + + private final IotUdpProtocol protocol; + + private final IotUdpSessionManager sessionManager; + + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + + /** + * 处理下行消息 + */ + public void handle(IotDeviceMessage message) { + try { + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + // 1. 检查设备会话 + IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSession(message.getDeviceId()); + if (sessionInfo == null) { + log.warn("[handle][会话信息不存在,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + return; + } + DatagramSocket socket = protocol.getUdpSocket(); + if (socket == null) { + log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId()); + return; + } + + // 2. 序列化消息 + byte[] serializedData = serializer.serialize(message); + + // 3. 发送到设备 + boolean success = sessionManager.sendToDevice(message.getDeviceId(), serializedData, socket); + if (!success) { + throw new RuntimeException("下行消息发送失败"); + } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), serializedData.length); + } catch (Exception e) { + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", + message.getDeviceId(), message.getMethod(), message, e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java new file mode 100644 index 000000000..a7907330f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 UDP 下游订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotUdpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { + + private final IotUdpDownstreamHandler downstreamHandler; + + public IotUdpDownstreamSubscriber(IotProtocol protocol, + IotUdpDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java new file mode 100644 index 000000000..9e6fcb320 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java @@ -0,0 +1,376 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.datagram.DatagramPacket; +import io.vertx.core.datagram.DatagramSocket; +import lombok.extern.slf4j.Slf4j; + +import java.net.InetSocketAddress; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * UDP 上行消息处理器 + *

+ * 采用无状态 Token 机制(每次请求携带 token): + * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password + * 2. 返回 Token:服务端验证后返回 JWT token + * 3. 后续请求:每次请求在 params 中携带 token + * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 + * + * @author 芋道源码 + */ +@Slf4j +public class IotUdpUpstreamHandler { + + private static final String AUTH_METHOD = "auth"; + + /** + * Token 参数 Key + */ + private static final String PARAM_KEY_TOKEN = "token"; + /** + * Body 参数 Key(实际请求内容) + */ + private static final String PARAM_KEY_BODY = "body"; + + private final String serverId; + + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + /** + * UDP 会话管理器 + */ + private final IotUdpSessionManager sessionManager; + + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + private final IotDeviceTokenService deviceTokenService; + private final IotDeviceCommonApi deviceApi; + + public IotUdpUpstreamHandler(String serverId, + IotUdpSessionManager sessionManager, + IotMessageSerializer serializer) { + Assert.notNull(serializer, "消息序列化器必须配置"); + Assert.notNull(sessionManager, "会话管理器不能为空"); + this.serverId = serverId; + this.sessionManager = sessionManager; + this.serializer = serializer; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + } + + /** + * 处理 UDP 数据包 + * + * @param packet 数据包 + * @param socket UDP Socket + */ + public void handle(DatagramPacket packet, DatagramSocket socket) { + InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); + Buffer data = packet.data(); + String addressKey = sessionManager.buildAddressKey(senderAddress); + log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", addressKey, data.length()); + processMessage(data, senderAddress, socket); + } + + /** + * 处理消息 + * + * @param buffer 消息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + */ + private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { + String addressKey = sessionManager.buildAddressKey(senderAddress); + // 1.1 基础检查 + if (ArrayUtil.isEmpty(buffer)) { + return; + } + // 1.2 反序列化消息 + IotDeviceMessage message = serializer.deserialize(buffer.getBytes()); + if (message == null) { + sendErrorResponse(socket, senderAddress, null, null, BAD_REQUEST.getCode(), "消息反序列化失败"); + return; + } + + // 2. 根据消息类型路由处理 + try { + if (AUTH_METHOD.equals(message.getMethod())) { + // 认证请求 + handleAuthenticationRequest(message, senderAddress, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + // 设备动态注册请求 + handleRegisterRequest(message, senderAddress, socket); + } else { + // 业务消息 + handleBusinessRequest(message, senderAddress, socket); + } + } catch (ServiceException e) { + // 业务异常,返回对应的错误码和错误信息 + log.warn("[processMessage][业务异常,来源: {},requestId: {},method: {},错误: {}]", + addressKey, message.getRequestId(), message.getMethod(), e.getMessage()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + // 参数校验失败,返回 400 + log.warn("[processMessage][参数校验失败,来源: {},requestId: {},method: {},错误: {}]", + addressKey, message.getRequestId(), message.getMethod(), e.getMessage()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + // 其他异常,返回 500 + log.error("[processMessage][处理消息失败,来源: {},requestId: {},method: {}]", + addressKey, message.getRequestId(), message.getMethod(), e); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + } + + /** + * 处理认证请求 + * + * @param message 消息信息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + */ + @SuppressWarnings("DuplicatedCode") + private void handleAuthenticationRequest(IotDeviceMessage message, InetSocketAddress senderAddress, + DatagramSocket socket) { + String clientId = IdUtil.simpleUUID(); + // 1. 解析认证参数 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); + Assert.notNull(authParams, "认证参数不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); + + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + authResult.checkError(); + if (!BooleanUtil.isTrue(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3. 生成 JWT Token(无状态) + String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); + + // 4.1 注册会话 + registerSession(senderAddress, device, clientId); + // 4.2 发送上线消息 + sendOnlineMessage(device); + // 4.3 发送成功响应(包含 token) + sendSuccessResponse(socket, senderAddress, message.getRequestId(), AUTH_METHOD, + MapUtil.of("token", token)); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", + device.getId(), device.getDeviceName(), sessionManager.buildAddressKey(senderAddress)); + } + + /** + * 处理设备动态注册请求(一型一密,不需要认证) + * + * @param message 消息信息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + * @see 阿里云 - 一型一密 + */ + @SuppressWarnings("DuplicatedCode") + private void handleRegisterRequest(IotDeviceMessage message, InetSocketAddress senderAddress, + DatagramSocket socket) { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 发送成功响应 + sendSuccessResponse(socket, senderAddress, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); + log.info("[handleRegisterRequest][注册成功,来源: {},设备名: {}]", + sessionManager.buildAddressKey(senderAddress), params.getDeviceName()); + } + + /** + * 处理业务请求 + *

+ * 请求参数格式: + * - token:JWT 令牌 + * - body:实际请求内容(可以是 Map、List 或其他类型) + * + * @param message 消息信息 + * @param senderAddress 发送者地址 + * @param socket UDP Socket + */ + @SuppressWarnings("unchecked") + private void handleBusinessRequest(IotDeviceMessage message, InetSocketAddress senderAddress, + DatagramSocket socket) { + String addressKey = sessionManager.buildAddressKey(senderAddress); + // 1.1 从消息中提取 token 和 body + String token = null; + Object body = null; + if (message.getParams() instanceof Map) { + Map paramsMap = (Map) message.getParams(); + token = (String) paramsMap.get(PARAM_KEY_TOKEN); + body = paramsMap.get(PARAM_KEY_BODY); + } + if (StrUtil.isBlank(token)) { + log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "请先进行认证"); + return; + } + // 1.2 验证 token,获取设备信息 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); + if (deviceInfo == null) { + log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "token 无效或已过期"); + return; + } + // 1.3 获取设备详细信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + if (device == null) { + log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", + addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + sendErrorResponse(socket, senderAddress, message.getRequestId(), message.getMethod(), + BAD_REQUEST.getCode(), "设备不存在"); + return; + } + + // 2. 更新会话地址(如有变化) + sessionManager.updateSessionAddress(device.getId(), senderAddress); + + // 3. 将 body 设置为实际的 params,发送消息到消息总线 + message.setParams(body); + deviceMessageService.sendDeviceMessage(message, device.getProductKey(), + device.getDeviceName(), serverId); + log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", + device.getId(), message.getMethod(), addressKey); + } + + /** + * 注册会话信息 + * + * @param address 设备地址 + * @param device 设备 + * @param clientId 客户端 ID + */ + private void registerSession(InetSocketAddress address, IotDeviceRespDTO device, String clientId) { + IotUdpSessionManager.SessionInfo sessionInfo = new IotUdpSessionManager.SessionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()) + .setAddress(address); + sessionManager.registerSession(device.getId(), sessionInfo); + } + + /** + * 发送设备上线消息 + * + * @param device 设备信息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + } + + // ===================== 发送响应消息 ===================== + + /** + * 发送成功响应 + * + * @param socket UDP Socket + * @param address 目标地址 + * @param requestId 请求 ID + * @param method 方法名 + * @param data 响应数据 + */ + private void sendSuccessResponse(DatagramSocket socket, InetSocketAddress address, + String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); + writeResponse(socket, address, responseMessage); + } + + /** + * 发送错误响应 + * + * @param socket UDP Socket + * @param address 目标地址 + * @param requestId 请求 ID + * @param method 方法名 + * @param code 错误码 + * @param msg 错误消息 + */ + private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, + String requestId, String method, Integer code, String msg) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); + writeResponse(socket, address, responseMessage); + } + + /** + * 写入响应到 Socket + * + * @param socket UDP Socket + * @param address 目标地址 + * @param responseMessage 响应消息 + */ + private void writeResponse(DatagramSocket socket, InetSocketAddress address, IotDeviceMessage responseMessage) { + try { + byte[] serializedData = serializer.serialize(responseMessage); + socket.send(Buffer.buffer(serializedData), address.getPort(), address.getHostString(), result -> { + if (result.failed()) { + log.error("[writeResponse][发送响应失败,地址: {}]", + sessionManager.buildAddressKey(address), result.cause()); + } + }); + } catch (Exception e) { + log.error("[writeResponse][发送响应异常,地址: {}]", + sessionManager.buildAddressKey(address), e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java index 79a5bf024..d807bce75 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java @@ -1,105 +1,101 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager; +import cn.hutool.core.util.ObjUtil; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import io.vertx.core.buffer.Buffer; import io.vertx.core.datagram.DatagramSocket; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.net.InetSocketAddress; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 网关 UDP 会话管理器 *

- * 采用无状态设计,SessionManager 主要用于: - * 1. 管理设备地址映射(用于下行消息发送) - * 2. 定期清理不活跃的设备地址映射 - *

- * 注意:UDP 是无连接协议,上行消息通过 token 验证身份,不依赖会话状态 + * 基于 Guava Cache 实现会话的自动过期清理: + * 1. 管理设备会话信息(设备 ID -> 地址映射) + * 2. 自动清理超时会话(expireAfterAccess) + * 3. 限制最大会话数(maximumSize) * * @author 芋道源码 */ @Slf4j -@Component public class IotUdpSessionManager { /** - * 设备 ID -> 会话信息(包含地址和 codecType) + * 设备会话缓存:设备 ID -> 会话信息 + *

+ * 使用 Guava Cache 自动管理过期:expireAfterAccess:每次访问(get/put)自动刷新过期时间 */ - private final Map deviceSessionMap = new ConcurrentHashMap<>(); + private final Cache deviceSessionCache; - /** - * 设备地址 Key -> 最后活跃时间(用于清理) - */ - private final Map lastActiveTimeMap = new ConcurrentHashMap<>(); + private final int maxSessions; - /** - * 设备地址 Key -> 设备 ID(反向映射,用于清理时同步) - */ - private final Map addressDeviceMap = new ConcurrentHashMap<>(); - - /** - * 更新设备会话(每次收到上行消息时调用) - * - * @param deviceId 设备 ID - * @param address 设备地址 - * @param codecType 消息编解码类型 - */ - public void updateDeviceSession(Long deviceId, InetSocketAddress address, String codecType) { - String addressKey = buildAddressKey(address); - // 更新设备会话映射 - deviceSessionMap.put(deviceId, new SessionInfo().setAddress(address).setCodecType(codecType)); - lastActiveTimeMap.put(addressKey, LocalDateTime.now()); - addressDeviceMap.put(addressKey, deviceId); - log.debug("[updateDeviceSession][更新设备会话,设备 ID: {},地址: {},codecType: {}]", deviceId, addressKey, codecType); + public IotUdpSessionManager(int maxSessions, long sessionTimeoutMs) { + this.maxSessions = maxSessions; + this.deviceSessionCache = CacheBuilder.newBuilder() + .maximumSize(maxSessions) + .expireAfterAccess(sessionTimeoutMs, TimeUnit.MILLISECONDS) + .build(); } /** - * 更新设备地址(兼容旧接口,默认不更新 codecType) + * 注册设备会话 * - * @param deviceId 设备 ID - * @param address 设备地址 + * @param deviceId 设备 ID + * @param sessionInfo 会话信息 */ - public void updateDeviceAddress(Long deviceId, InetSocketAddress address) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); - String codecType = sessionInfo != null ? sessionInfo.getCodecType() : null; - updateDeviceSession(deviceId, address, codecType); + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) { + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性) + if (deviceSessionCache.getIfPresent(deviceId) == null + && deviceSessionCache.size() >= maxSessions) { + throw new IllegalStateException("会话数已达上限: " + maxSessions); + } + // 注册会话 + deviceSessionCache.put(deviceId, sessionInfo); + log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},productKey: {},deviceName: {}]", + deviceId, buildAddressKey(sessionInfo.getAddress()), + sessionInfo.getProductKey(), sessionInfo.getDeviceName()); } /** - * 获取设备会话信息 + * 获取会话信息 + *

+ * 注意:调用此方法会自动刷新会话的过期时间 * * @param deviceId 设备 ID - * @return 会话信息 + * @return 会话信息,不存在则返回 null */ - public SessionInfo getSessionInfo(Long deviceId) { - return deviceSessionMap.get(deviceId); + public SessionInfo getSession(Long deviceId) { + return deviceSessionCache.getIfPresent(deviceId); } /** - * 检查设备是否在线(即是否有地址映射) + * 更新设备会话地址(设备地址变更时调用) + *

+ * 注意:getIfPresent 已自动刷新过期时间,无需重新 put * - * @param deviceId 设备 ID - * @return 是否在线 + * @param deviceId 设备 ID + * @param newAddress 新地址 */ - public boolean isDeviceOnline(Long deviceId) { - return deviceSessionMap.containsKey(deviceId); - } + public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) { + // 地址未变化,无需更新 + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); + if (sessionInfo == null) { + return; + } + if (ObjUtil.equals(newAddress, sessionInfo.getAddress())) { + return; + } - /** - * 检查设备是否离线 - * - * @param deviceId 设备 ID - * @return 是否离线 - */ - public boolean isDeviceOffline(Long deviceId) { - return !isDeviceOnline(deviceId); + // 更新地址 + String oldAddressKey = buildAddressKey(sessionInfo.getAddress()); + sessionInfo.setAddress(newAddress); + log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},旧地址: {},新地址: {}]", + deviceId, oldAddressKey, buildAddressKey(newAddress)); } /** @@ -111,24 +107,28 @@ public class IotUdpSessionManager { * @return 是否发送成功 */ public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) { - SessionInfo sessionInfo = deviceSessionMap.get(deviceId); + SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId); if (sessionInfo == null || sessionInfo.getAddress() == null) { log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId); return false; } - InetSocketAddress address = sessionInfo.getAddress(); try { + // 使用 CompletableFuture 同步等待发送结果 + CompletableFuture future = new CompletableFuture<>(); socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { if (result.succeeded()) { log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", deviceId, buildAddressKey(address), data.length); + future.complete(true); } else { log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", deviceId, buildAddressKey(address), result.cause()); + future.complete(false); } }); - return true; + // 同步等待结果,超时 5 秒 + return future.get(5, TimeUnit.SECONDS); } catch (Exception e) { log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); return false; @@ -136,44 +136,7 @@ public class IotUdpSessionManager { } /** - * 定期清理不活跃的设备地址映射 - * - * @param timeoutMs 超时时间(毫秒) - * @return 清理的设备 ID 列表(用于发送离线消息) - */ - public List cleanExpiredMappings(long timeoutMs) { - List offlineDeviceIds = new ArrayList<>(); - LocalDateTime now = LocalDateTime.now(); - LocalDateTime expireTime = now.minusNanos(timeoutMs * 1_000_000); - Iterator> iterator = lastActiveTimeMap.entrySet().iterator(); - while (iterator.hasNext()) { - // 未过期,跳过 - Map.Entry entry = iterator.next(); - if (entry.getValue().isAfter(expireTime)) { - continue; - } - // 过期处理:记录离线设备 ID - String addressKey = entry.getKey(); - Long deviceId = addressDeviceMap.remove(addressKey); - if (deviceId == null) { - iterator.remove(); - continue; - } - SessionInfo sessionInfo = deviceSessionMap.remove(deviceId); - if (sessionInfo == null) { - iterator.remove(); - continue; - } - offlineDeviceIds.add(deviceId); - log.debug("[cleanExpiredMappings][清理超时设备,设备 ID: {},地址: {},最后活跃时间: {}]", - deviceId, addressKey, entry.getValue()); - iterator.remove(); - } - return offlineDeviceIds; - } - - /** - * 构建地址 Key + * 构建地址 Key(用于日志输出) * * @param address 地址 * @return 地址 Key @@ -188,16 +151,24 @@ public class IotUdpSessionManager { @Data public static class SessionInfo { + /** + * 设备 ID + */ + private Long deviceId; + /** + * 产品 Key + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + /** * 设备地址 */ private InetSocketAddress address; - /** - * 消息编解码类型 - */ - private String codecType; - } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java deleted file mode 100644 index 6aeb2cb7a..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpDownstreamHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.datagram.DatagramSocket; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 UDP 下行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpDownstreamHandler { - - private final IotDeviceMessageService deviceMessageService; - - private final IotUdpSessionManager sessionManager; - - private final IotUdpUpstreamProtocol protocol; - - public IotUdpDownstreamHandler(IotDeviceMessageService deviceMessageService, - IotUdpSessionManager sessionManager, - IotUdpUpstreamProtocol protocol) { - this.deviceMessageService = deviceMessageService; - this.sessionManager = sessionManager; - this.protocol = protocol; - } - - /** - * 处理下行消息 - * - * @param message 下行消息 - */ - public void handle(IotDeviceMessage message) { - try { - log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); - - // 1. 获取会话信息(包含 codecType) - IotUdpSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(message.getDeviceId()); - if (sessionInfo == null) { - log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); - return; - } - - // 2. 使用会话中的 codecType 编码消息,并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, sessionInfo.getCodecType()); - DatagramSocket socket = protocol.getUdpSocket(); - if (socket == null) { - log.error("[handle][UDP Socket 不可用,设备 ID: {}]", message.getDeviceId()); - return; - } - boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes, socket); - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); - } - } catch (Exception e) { - log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", - message.getDeviceId(), message.getMethod(), message, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java deleted file mode 100644 index 872a615a6..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/router/IotUdpUpstreamHandler.java +++ /dev/null @@ -1,542 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.udp.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.datagram.DatagramPacket; -import io.vertx.core.datagram.DatagramSocket; -import lombok.extern.slf4j.Slf4j; - -import java.net.InetSocketAddress; -import java.util.Map; - -/** - * UDP 上行消息处理器 - *

- * 采用无状态 Token 机制(每次请求携带 token): - * 1. 认证请求:设备发送 auth 消息,携带 clientId、username、password - * 2. 返回 Token:服务端验证后返回 JWT token - * 3. 后续请求:每次请求在 params 中携带 token - * 4. 服务端验证:每次请求通过 IotDeviceTokenService.verifyToken() 验证 - * - * @author 芋道源码 - */ -@Slf4j -public class IotUdpUpstreamHandler { - - private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; - private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; - - private static final String AUTH_METHOD = "auth"; - /** - * Token 参数 Key - */ - private static final String PARAM_KEY_TOKEN = "token"; - /** - * Body 参数 Key(实际请求内容) - */ - private static final String PARAM_KEY_BODY = "body"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotUdpSessionManager sessionManager; - - private final IotDeviceTokenService deviceTokenService; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotUdpUpstreamHandler(IotUdpUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotUdpSessionManager sessionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.sessionManager = sessionManager; - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - } - - /** - * 处理 UDP 数据包 - * - * @param packet 数据包 - * @param socket UDP Socket - */ - public void handle(DatagramPacket packet, DatagramSocket socket) { - InetSocketAddress senderAddress = new InetSocketAddress(packet.sender().host(), packet.sender().port()); - Buffer data = packet.data(); - log.debug("[handle][收到 UDP 数据包,来源: {},数据长度: {} 字节]", - sessionManager.buildAddressKey(senderAddress), data.length()); - try { - processMessage(data, senderAddress, socket); - } catch (Exception e) { - log.error("[handle][处理消息失败,来源: {},错误: {}]", - sessionManager.buildAddressKey(senderAddress), e.getMessage(), e); - // UDP 无连接,不需要断开连接,只记录错误 - } - } - - /** - * 处理消息 - * - * @param buffer 消息 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - private void processMessage(Buffer buffer, InetSocketAddress senderAddress, DatagramSocket socket) { - // 1. 基础检查 - if (buffer == null || buffer.length() == 0) { - return; - } - - // 2. 获取消息格式类型 - String codecType = getMessageCodecType(buffer); - - // 3. 解码消息 - IotDeviceMessage message; - try { - message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); - if (message == null) { - log.warn("[processMessage][消息解码失败,来源: {}]", sessionManager.buildAddressKey(senderAddress)); - sendErrorResponse(socket, senderAddress, null, "消息解码失败", codecType); - return; - } - } catch (Exception e) { - log.error("[processMessage][消息解码异常,来源: {}]", sessionManager.buildAddressKey(senderAddress), e); - sendErrorResponse(socket, senderAddress, null, "消息解码失败: " + e.getMessage(), codecType); - return; - } - - // 4. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(message.getMethod())) { - // 认证请求 - handleAuthenticationRequest(message, codecType, senderAddress, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(message, codecType, senderAddress, socket); - } else { - // 业务消息 - handleBusinessRequest(message, codecType, senderAddress, socket); - } - } catch (Exception e) { - log.error("[processMessage][处理消息失败,来源: {},消息方法: {}]", - sessionManager.buildAddressKey(senderAddress), message.getMethod(), e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "消息处理失败", codecType); - } - } - - /** - * 处理认证请求 - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - private void handleAuthenticationRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1.1 解析认证参数 - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证参数不完整", codecType); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,来源: {},username: {}]", - addressKey, authParams.getUsername()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证失败", codecType); - return; - } - - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, senderAddress, message.getRequestId(), "解析设备信息失败", codecType); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3.1 生成 JWT Token(无状态) - String token = deviceTokenService.createToken(device.getProductKey(), device.getDeviceName()); - - // 3.2 更新设备会话信息(用于下行消息,保存 codecType) - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); - - // 3.3 发送上线消息 - sendOnlineMessage(device); - - // 3.4 发送成功响应(包含 token) - sendAuthSuccessResponse(socket, senderAddress, message.getRequestId(), token, codecType); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {},来源: {}]", - device.getId(), device.getDeviceName(), addressKey); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "认证处理异常", codecType); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要 Token) - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null) { - log.warn("[handleRegisterRequest][注册参数解析失败,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册参数不完整", codecType); - return; - } - - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,来源: {},错误: {}]", addressKey, result.getMsg()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), result.getMsg(), codecType); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, senderAddress, message.getRequestId(), result.getData(), codecType); - log.info("[handleRegisterRequest][注册成功,设备名: {},来源: {}]", - params.getDeviceName(), addressKey); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "注册处理异常", codecType); - } - } - - /** - * 处理业务请求 - *

- * 请求参数格式: - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param message 消息信息 - * @param codecType 消息编解码类型 - * @param senderAddress 发送者地址 - * @param socket UDP Socket - */ - @SuppressWarnings("unchecked") - private void handleBusinessRequest(IotDeviceMessage message, String codecType, - InetSocketAddress senderAddress, DatagramSocket socket) { - String addressKey = sessionManager.buildAddressKey(senderAddress); - try { - // 1.1 从消息中提取 token 和 body(格式:{token: "xxx", body: {...}} 或 {token: "xxx", body: [...]}) - String token = null; - Object body = null; - if (message.getParams() instanceof Map) { - Map paramsMap = (Map) message.getParams(); - token = (String) paramsMap.get(PARAM_KEY_TOKEN); - body = paramsMap.get(PARAM_KEY_BODY); - } - if (StrUtil.isBlank(token)) { - log.warn("[handleBusinessRequest][缺少 token,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "请先进行认证", codecType); - return; - } - // 1.2 验证 token,获取设备信息 - IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token); - if (deviceInfo == null) { - log.warn("[handleBusinessRequest][token 无效或已过期,来源: {}]", addressKey); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "token 无效或已过期", codecType); - return; - } - - // 2. 获取设备详细信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - log.warn("[handleBusinessRequest][设备不存在,来源: {},productKey: {},deviceName: {}]", - addressKey, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "设备不存在", codecType); - return; - } - - // 3. 更新设备会话信息(保持最新,保存 codecType) - sessionManager.updateDeviceSession(device.getId(), senderAddress, codecType); - - // 4. 将 body 设置为实际的 params,发送消息到消息总线 - message.setParams(body); - deviceMessageService.sendDeviceMessage(message, device.getProductKey(), - device.getDeviceName(), serverId); - log.debug("[handleBusinessRequest][业务消息处理成功,设备 ID: {},方法: {},来源: {}]", - device.getId(), message.getMethod(), addressKey); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,来源: {}]", addressKey, e); - sendErrorResponse(socket, senderAddress, message.getRequestId(), "处理失败", codecType); - } - } - - /** - * 获取消息编解码类型 - * - * @param buffer 消息 - * @return 消息编解码类型 - */ - private String getMessageCodecType(Buffer buffer) { - // 检测消息格式类型 - return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY - : CODEC_TYPE_JSON; - } - - /** - * 发送设备上线消息 - * - * @param device 设备信息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } - } - - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送认证成功响应(包含 token) - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param token JWT Token - * @param codecType 消息编解码类型 - */ - private void sendAuthSuccessResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, String token, String codecType) { - try { - // 构建响应数据 - Object responseData = MapUtil.builder() - .put("success", true) - .put("token", token) - .put("message", "认证成功") - .build(); - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, 0, "认证成功"); - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - // 发送响应 - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { - if (result.failed()) { - log.error("[sendAuthSuccessResponse][发送认证成功响应失败,地址: {}]", - sessionManager.buildAddressKey(address), result.cause()); - } - }); - } catch (Exception e) { - log.error("[sendAuthSuccessResponse][发送认证成功响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param registerResp 注册响应 - * @param codecType 消息编解码类型 - */ - private void sendRegisterSuccessResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, IotDeviceRegisterRespDTO registerResp, - String codecType) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), result -> { - if (result.failed()) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,地址: {}]", - sessionManager.buildAddressKey(address), result.cause()); - } - }); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 发送错误响应 - * - * @param socket UDP Socket - * @param address 目标地址 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - * @param codecType 消息编解码类型 - */ - private void sendErrorResponse(DatagramSocket socket, InetSocketAddress address, - String requestId, String errorMessage, String codecType) { - sendResponse(socket, address, false, errorMessage, requestId, codecType); - } - - /** - * 发送响应消息 - * - * @param socket UDP Socket - * @param address 目标地址 - * @param success 是否成功 - * @param message 消息 - * @param requestId 请求 ID - * @param codecType 消息编解码类型 - */ - @SuppressWarnings("SameParameterValue") - private void sendResponse(DatagramSocket socket, InetSocketAddress address, boolean success, - String message, String requestId, String codecType) { - try { - // 构建响应数据 - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - "response", responseData, code, message); - - // 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); - socket.send(Buffer.buffer(encodedData), address.getPort(), address.getHostString(), ar -> { - if (ar.failed()) { - log.error("[sendResponse][发送响应失败,地址: {}]", - sessionManager.buildAddressKey(address), ar.cause()); - } - }); - } catch (Exception e) { - log.error("[sendResponse][发送响应异常,地址: {}]", - sessionManager.buildAddressKey(address), e); - } - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings("unchecked") - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java new file mode 100644 index 000000000..95f738d0a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketConfig.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT WebSocket 协议配置 + * + * @author 芋道源码 + */ +@Data +public class IotWebSocketConfig { + + /** + * WebSocket 路径(默认:/ws) + */ + @NotEmpty(message = "WebSocket 路径不能为空") + private String path = "/ws"; + + /** + * 最大消息大小(字节,默认 64KB) + */ + @NotNull(message = "最大消息大小不能为空") + private Integer maxMessageSize = 65536; + /** + * 最大帧大小(字节,默认 64KB) + */ + @NotNull(message = "最大帧大小不能为空") + private Integer maxFrameSize = 65536; + + /** + * 空闲超时时间(秒,默认 60) + */ + @NotNull(message = "空闲超时时间不能为空") + private Integer idleTimeoutSeconds = 60; + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java deleted file mode 100644 index 47abb331a..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 WebSocket 下游订阅者:接收下行给设备的消息 - * - * @author 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotWebSocketDownstreamSubscriber implements IotMessageSubscriber { - - private final IotWebSocketUpstreamProtocol protocol; - - private final IotDeviceMessageService messageService; - - private final IotWebSocketConnectionManager connectionManager; - - private final IotMessageBus messageBus; - - private IotWebSocketDownstreamHandler downstreamHandler; - - @PostConstruct - public void init() { - // 初始化下游处理器 - this.downstreamHandler = new IotWebSocketDownstreamHandler(messageService, connectionManager); - // 注册下游订阅者 - messageBus.register(this); - log.info("[init][WebSocket 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", - protocol.getServerId(), getTopic()); - } - - @Override - public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); - } - - @Override - public String getGroup() { - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group - return getTopic(); - } - - @Override - public void onMessage(IotDeviceMessage message) { - try { - downstreamHandler.handle(message); - } catch (Exception e) { - log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId(), e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java new file mode 100644 index 000000000..083dc3236 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java @@ -0,0 +1,208 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream.IotWebSocketUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT WebSocket 协议实现 + *

+ * 基于 Vert.x 实现 WebSocket 服务器,接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotWebSocketProtocol implements IotProtocol { + + /** + * 协议配置 + */ + private final ProtocolProperties properties; + /** + * 服务器 ID(用于消息追踪,全局唯一) + */ + @Getter + private final String serverId; + + /** + * 运行状态 + */ + @Getter + private volatile boolean running = false; + + /** + * Vert.x 实例 + */ + private Vertx vertx; + /** + * WebSocket 服务器 + */ + private HttpServer httpServer; + /** + * WebSocket 连接管理器 + */ + private final IotWebSocketConnectionManager connectionManager; + + /** + * 下行消息订阅者 + */ + private IotWebSocketDownstreamSubscriber downstreamSubscriber; + + /** + * 消息序列化器 + */ + private final IotMessageSerializer serializer; + + public IotWebSocketProtocol(ProtocolProperties properties) { + Assert.notNull(properties, "协议实例配置不能为空"); + Assert.notNull(properties.getWebsocket(), "WebSocket 协议配置(websocket)不能为空"); + this.properties = properties; + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); + + // 初始化序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(properties.getSerialize()); + Assert.notNull(serializeType, "不支持的序列化类型:" + properties.getSerialize()); + IotMessageSerializerManager serializerManager = SpringUtil.getBean(IotMessageSerializerManager.class); + this.serializer = serializerManager.get(serializeType); + + // 初始化连接管理器 + this.connectionManager = new IotWebSocketConnectionManager(); + + } + + @Override + public String getId() { + return properties.getId(); + } + + @Override + public IotProtocolTypeEnum getType() { + return IotProtocolTypeEnum.WEBSOCKET; + } + + @Override + @SuppressWarnings("deprecation") + public void start() { + if (running) { + log.warn("[start][IoT WebSocket 协议 {} 已经在运行中]", getId()); + return; + } + + // 1.1 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + + // 1.2 创建服务器选项 + IotWebSocketConfig wsConfig = properties.getWebsocket(); + HttpServerOptions options = new HttpServerOptions() + .setPort(properties.getPort()) + .setIdleTimeout(wsConfig.getIdleTimeoutSeconds()) + .setMaxWebSocketFrameSize(wsConfig.getMaxFrameSize()) + .setMaxWebSocketMessageSize(wsConfig.getMaxMessageSize()); + IotGatewayProperties.SslConfig sslConfig = properties.getSsl(); + if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(sslConfig.getSslKeyPath()) + .setCertPath(sslConfig.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + + // 1.3 创建服务器并设置 WebSocket 处理器 + httpServer = vertx.createHttpServer(options); + httpServer.webSocketHandler(socket -> { + // 验证路径 + if (ObjUtil.notEqual(wsConfig.getPath(), socket.path())) { + log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", + socket.path(), wsConfig.getPath()); + socket.reject(); + return; + } + // 创建上行处理器 + IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(serverId, serializer, connectionManager); + handler.handle(socket); + }); + + // 1.4 启动服务器 + try { + httpServer.listen().result(); + running = true; + log.info("[start][IoT WebSocket 协议 {} 启动成功,端口:{},路径:{},serverId:{}]", + getId(), properties.getPort(), wsConfig.getPath(), serverId); + + // 2. 启动下行消息订阅者 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); + IotWebSocketDownstreamHandler downstreamHandler = new IotWebSocketDownstreamHandler(serializer, connectionManager); + this.downstreamSubscriber = new IotWebSocketDownstreamSubscriber(this, downstreamHandler, messageBus); + downstreamSubscriber.start(); + } catch (Exception e) { + log.error("[start][IoT WebSocket 协议 {} 启动失败]", getId(), e); + stop0(); + throw e; + } + } + + @Override + public void stop() { + if (!running) { + return; + } + stop0(); + } + + private void stop0() { + // 1. 停止下行消息订阅者 + if (downstreamSubscriber != null) { + try { + downstreamSubscriber.stop(); + log.info("[stop][IoT WebSocket 协议 {} 下行消息订阅者已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} 下行消息订阅者停止失败]", getId(), e); + } + downstreamSubscriber = null; + } + + // 2.1 关闭所有连接 + connectionManager.closeAll(); + // 2.2 关闭 WebSocket 服务器 + if (httpServer != null) { + try { + httpServer.close().result(); + log.info("[stop][IoT WebSocket 协议 {} 服务器已停止]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} 服务器停止失败]", getId(), e); + } + httpServer = null; + } + // 2.3 关闭 Vertx 实例 + if (vertx != null) { + try { + vertx.close().result(); + log.info("[stop][IoT WebSocket 协议 {} Vertx 已关闭]", getId()); + } catch (Exception e) { + log.error("[stop][IoT WebSocket 协议 {} Vertx 关闭失败]", getId(), e); + } + vertx = null; + } + running = false; + log.info("[stop][IoT WebSocket 协议 {} 已停止]", getId()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java deleted file mode 100644 index 9c612acec..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket; - -import cn.hutool.core.util.ObjUtil; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router.IotWebSocketUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.net.PemKeyCertOptions; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 网关 WebSocket 协议:接收设备上行消息 - * - * @author 芋道源码 - */ -@Slf4j -public class IotWebSocketUpstreamProtocol { - - private final IotGatewayProperties.WebSocketProperties wsProperties; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotWebSocketConnectionManager connectionManager; - - private final Vertx vertx; - - @Getter - private final String serverId; - - private HttpServer httpServer; - - public IotWebSocketUpstreamProtocol(IotGatewayProperties.WebSocketProperties wsProperties, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotWebSocketConnectionManager connectionManager, - Vertx vertx) { - this.wsProperties = wsProperties; - this.deviceService = deviceService; - this.messageService = messageService; - this.connectionManager = connectionManager; - this.vertx = vertx; - this.serverId = IotDeviceMessageUtils.generateServerId(wsProperties.getPort()); - } - - @PostConstruct - @SuppressWarnings("deprecation") - public void start() { - // 1.1 创建服务器选项 - HttpServerOptions options = new HttpServerOptions() - .setPort(wsProperties.getPort()) - .setIdleTimeout(wsProperties.getIdleTimeoutSeconds()) - .setMaxWebSocketFrameSize(wsProperties.getMaxFrameSize()) - .setMaxWebSocketMessageSize(wsProperties.getMaxMessageSize()); - // 1.2 配置 SSL(如果启用) - if (Boolean.TRUE.equals(wsProperties.getSslEnabled())) { - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setKeyPath(wsProperties.getSslKeyPath()) - .setCertPath(wsProperties.getSslCertPath()); - options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); - } - - // 2. 创建服务器并设置 WebSocket 处理器 - httpServer = vertx.createHttpServer(options); - httpServer.webSocketHandler(socket -> { - // 验证路径 - if (ObjUtil.notEqual(wsProperties.getPath(), socket.path())) { - log.warn("[webSocketHandler][WebSocket 路径不匹配,拒绝连接,路径: {},期望: {}]", - socket.path(), wsProperties.getPath()); - socket.reject(); - return; - } - // 创建上行处理器 - IotWebSocketUpstreamHandler handler = new IotWebSocketUpstreamHandler(this, - messageService, deviceService, connectionManager); - handler.handle(socket); - }); - - // 3. 启动服务器 - try { - httpServer.listen().result(); - log.info("[start][IoT 网关 WebSocket 协议启动成功,端口:{},路径:{}]", wsProperties.getPort(), wsProperties.getPath()); - } catch (Exception e) { - log.error("[start][IoT 网关 WebSocket 协议启动失败]", e); - throw e; - } - } - - @PreDestroy - public void stop() { - if (httpServer != null) { - try { - httpServer.close().result(); - log.info("[stop][IoT 网关 WebSocket 协议已停止]"); - } catch (Exception e) { - log.error("[stop][IoT 网关 WebSocket 协议停止失败]", e); - } - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java similarity index 58% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java index 05e3c8c91..45cf7da70 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamHandler.java @@ -1,9 +1,9 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,7 +16,7 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class IotWebSocketDownstreamHandler { - private final IotDeviceMessageService deviceMessageService; + private final IotMessageSerializer serializer; private final IotWebSocketConnectionManager connectionManager; @@ -36,17 +36,17 @@ public class IotWebSocketDownstreamHandler { return; } - // 2. 编码消息并发送到设备 - byte[] bytes = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getCodecType()); - String jsonMessage = StrUtil.utf8Str(bytes); - boolean success = connectionManager.sendToDevice(message.getDeviceId(), jsonMessage); - if (success) { - log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", - message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); - } else { - log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", - message.getDeviceId(), message.getMethod(), message.getId()); + // 2. 序列化 + byte[] bytes = serializer.serialize(message); + String bytesContent = StrUtil.utf8Str(bytes); + + // 3. 发送到设备 + boolean success = connectionManager.sendToDevice(connectionInfo.getDeviceId(), bytesContent); + if (!success) { + throw new RuntimeException("下行消息发送失败"); } + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); } catch (Exception e) { log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", message.getDeviceId(), message.getMethod(), message, e); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java new file mode 100644 index 000000000..c565be2c9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 WebSocket 下游订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotWebSocketDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { + + private final IotWebSocketDownstreamHandler downstreamHandler; + + public IotWebSocketDownstreamSubscriber(IotWebSocketProtocol protocol, + IotWebSocketDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + super(protocol, messageBus); + this.downstreamHandler = downstreamHandler; + } + + @Override + protected void handleMessage(IotDeviceMessage message) { + downstreamHandler.handle(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java new file mode 100644 index 000000000..bf0f55ccb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java @@ -0,0 +1,305 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Handler; +import io.vertx.core.http.ServerWebSocket; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + + +/** + * WebSocket 上行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotWebSocketUpstreamHandler implements Handler { + + private static final String AUTH_METHOD = "auth"; + + private final String serverId; + + /** + * 消息序列化器(处理业务消息序列化/反序列化) + */ + private final IotMessageSerializer serializer; + /** + * 连接管理器 + */ + private final IotWebSocketConnectionManager connectionManager; + + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + private final IotDeviceCommonApi deviceApi; + + public IotWebSocketUpstreamHandler(String serverId, + IotMessageSerializer serializer, + IotWebSocketConnectionManager connectionManager) { + this.serverId = serverId; + this.serializer = serializer; + this.connectionManager = connectionManager; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + @SuppressWarnings("DuplicatedCode") + public void handle(ServerWebSocket socket) { + String remoteAddress = String.valueOf(socket.remoteAddress()); + log.debug("[handle][设备连接,地址: {}]", remoteAddress); + + // 1. 设置异常和关闭处理器 + socket.exceptionHandler(ex -> { + log.warn("[handle][连接异常,地址: {}]", remoteAddress, ex); + socket.close(); + }); + socket.closeHandler(v -> { + log.debug("[handle][连接关闭,地址: {}]", remoteAddress); + cleanupConnection(socket); + }); + + // 2. 设置消息处理器(仅支持文本帧) + socket.textMessageHandler(message -> { + try { + processMessage(StrUtil.utf8Bytes(message), socket); + } catch (Exception e) { + log.error("[handle][消息解码失败,断开连接,地址: {},错误: {}]", remoteAddress, e.getMessage()); + socket.close(); + } + }); + } + + /** + * 处理消息 + * + * @param payload 消息负载 + * @param socket WebSocket 连接 + */ + private void processMessage(byte[] payload, ServerWebSocket socket) { + IotDeviceMessage message = null; + try { + // 1.1 基础检查 + if (ArrayUtil.isEmpty(payload)) { + return; + } + // 1.2 解码消息 + message = serializer.deserialize(payload); + Assert.notNull(message, "消息反序列化失败"); + Assert.notBlank(message.getMethod(), "method 不能为空"); + + // 2. 根据消息类型路由处理 + if (AUTH_METHOD.equals(message.getMethod())) { + handleAuthenticationRequest(message, socket); + } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(message.getMethod())) { + handleRegisterRequest(message, socket); + } else { + handleBusinessRequest(message, socket); + } + } catch (ServiceException e) { + log.warn("[processMessage][业务异常,错误: {}]", e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, e.getCode(), e.getMessage()); + } catch (IllegalArgumentException e) { + log.warn("[processMessage][参数校验失败,错误: {}]", e.getMessage()); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, BAD_REQUEST.getCode(), e.getMessage()); + } catch (Exception e) { + log.error("[processMessage][处理消息失败]", e); + String requestId = message != null ? message.getRequestId() : null; + String method = message != null ? message.getMethod() : null; + sendErrorResponse(socket, requestId, method, INTERNAL_SERVER_ERROR.getCode(), + INTERNAL_SERVER_ERROR.getMsg()); + throw e; + } + } + + /** + * 处理认证请求 + * + * @param message 消息信息 + * @param socket WebSocket 连接 + */ + @SuppressWarnings("DuplicatedCode") + private void handleAuthenticationRequest(IotDeviceMessage message, ServerWebSocket socket) { + // 1. 解析认证参数 + IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); + Assert.notNull(authParams, "认证参数不能为空"); + Assert.notBlank(authParams.getUsername(), "username 不能为空"); + Assert.notBlank(authParams.getPassword(), "password 不能为空"); + + // 2.1 执行认证 + CommonResult authResult = deviceApi.authDevice(authParams); + authResult.checkError(); + if (BooleanUtil.isFalse(authResult.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 解析设备信息 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + Assert.notNull(deviceInfo, "解析设备信息失败"); + // 2.3 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notNull(device, "设备不存在"); + + // 3.1 注册连接 + registerConnection(socket, device); + // 3.2 发送上线消息 + sendOnlineMessage(device); + // 3.3 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), AUTH_METHOD, "认证成功"); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", device.getId(), device.getDeviceName()); + } + + /** + * 处理设备动态注册请求(一型一密,不需要认证) + * + * @param message 消息信息 + * @param socket WebSocket 连接 + * @see 阿里云 - 一型一密 + */ + @SuppressWarnings("DuplicatedCode") + private void handleRegisterRequest(IotDeviceMessage message, ServerWebSocket socket) { + // 1. 解析注册参数 + IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); + Assert.notNull(params, "注册参数不能为空"); + Assert.notBlank(params.getProductKey(), "productKey 不能为空"); + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空"); + Assert.notBlank(params.getSign(), "sign 不能为空"); + + // 2. 调用动态注册 + CommonResult result = deviceApi.registerDevice(params); + result.checkError(); + + // 3. 发送成功响应(包含 deviceSecret) + sendSuccessResponse(socket, message.getRequestId(), + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), result.getData()); + log.info("[handleRegisterRequest][注册成功,设备名: {}]", params.getDeviceName()); + } + + /** + * 处理业务请求 + * + * @param message 消息信息 + * @param socket WebSocket 连接 + */ + private void handleBusinessRequest(IotDeviceMessage message, ServerWebSocket socket) { + // 1. 获取认证信息并处理业务消息 + IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo == null) { + log.warn("[handleBusinessRequest][连接未认证,拒绝处理业务消息]"); + sendErrorResponse(socket, message.getRequestId(), message.getMethod(), + UNAUTHORIZED.getCode(), "设备未认证,无法处理业务消息"); + return; + } + + // 2. 发送消息到消息总线 + deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + log.info("[handleBusinessRequest][发送消息到消息总线,消息: {}]", message); + } + + /** + * 注册连接信息 + * + * @param socket WebSocket 连接 + * @param device 设备 + */ + private void registerConnection(ServerWebSocket socket, IotDeviceRespDTO device) { + IotWebSocketConnectionManager.ConnectionInfo connectionInfo = new IotWebSocketConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()); + connectionManager.registerConnection(socket, device.getId(), connectionInfo); + } + + /** + * 发送设备上线消息 + * + * @param device 设备信息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + try { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + } catch (Exception e) { + log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); + } + } + + /** + * 清理连接 + * + * @param socket WebSocket 连接 + */ + private void cleanupConnection(ServerWebSocket socket) { + try { + // 1. 发送离线消息(如果已认证) + IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + } + + // 2. 注销连接 + connectionManager.unregisterConnection(socket); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败]", e); + } + } + + // ===================== 发送响应消息 ===================== + + /** + * 发送响应消息 + * + * @param socket WebSocket 连接 + * @param requestId 请求 ID + * @param method 请求方法 + * @param data 响应数据 + */ + private void sendSuccessResponse(ServerWebSocket socket, String requestId, String method, Object data) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, SUCCESS.getCode(), null); + writeResponse(socket, responseMessage); + } + + private void sendErrorResponse(ServerWebSocket socket, String requestId, String method, Integer code, String msg) { + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, code, msg); + writeResponse(socket, responseMessage); + } + + /** + * 写入响应消息 + */ + private void writeResponse(ServerWebSocket socket, IotDeviceMessage responseMessage) { + byte[] payload = serializer.serialize(responseMessage); + socket.writeTextMessage(StrUtil.utf8Str(payload)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java index 128b36008..92019ffad 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java @@ -4,8 +4,9 @@ import io.vertx.core.http.ServerWebSocket; import lombok.Data; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -20,7 +21,6 @@ import java.util.concurrent.ConcurrentHashMap; * @author 芋道源码 */ @Slf4j -@Component public class IotWebSocketConnectionManager { /** @@ -69,7 +69,8 @@ public class IotWebSocketConnectionManager { return; } Long deviceId = connectionInfo.getDeviceId(); - deviceSocketMap.remove(deviceId); + // 仅当 deviceSocketMap 中的 socket 是当前 socket 时才移除,避免误删新连接 + deviceSocketMap.remove(deviceId, socket); log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); } @@ -115,6 +116,24 @@ public class IotWebSocketConnectionManager { } } + /** + * 关闭所有连接 + */ + public void closeAll() { + // 1. 先复制再清空,避免 closeHandler 回调时并发修改 + List sockets = new ArrayList<>(connectionMap.keySet()); + connectionMap.clear(); + deviceSocketMap.clear(); + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过) + for (ServerWebSocket socket : sockets) { + try { + socket.close(); + } catch (Exception ignored) { + // 连接可能已关闭,忽略异常 + } + } + } + /** * 连接信息(包含认证信息) */ @@ -135,15 +154,6 @@ public class IotWebSocketConnectionManager { */ private String deviceName; - /** - * 客户端 ID - */ - private String clientId; - /** - * 消息编解码类型(认证后确定) - */ - private String codecType; - } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java deleted file mode 100644 index 630246afa..000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/router/IotWebSocketUpstreamHandler.java +++ /dev/null @@ -1,471 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.router; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import io.vertx.core.Handler; -import io.vertx.core.http.ServerWebSocket; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - - -/** - * WebSocket 上行消息处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotWebSocketUpstreamHandler implements Handler { - - /** - * 默认消息编解码类型 - */ - private static final String CODEC_TYPE = IotAlinkDeviceMessageCodec.TYPE; - - private static final String AUTH_METHOD = "auth"; - - private final IotDeviceMessageService deviceMessageService; - - private final IotDeviceService deviceService; - - private final IotWebSocketConnectionManager connectionManager; - - private final IotDeviceCommonApi deviceApi; - - private final String serverId; - - public IotWebSocketUpstreamHandler(IotWebSocketUpstreamProtocol protocol, - IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, - IotWebSocketConnectionManager connectionManager) { - this.deviceMessageService = deviceMessageService; - this.deviceService = deviceService; - this.connectionManager = connectionManager; - this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.serverId = protocol.getServerId(); - } - - @Override - public void handle(ServerWebSocket socket) { - String clientId = IdUtil.simpleUUID(); - log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - - // 1. 设置异常和关闭处理器 - socket.exceptionHandler(ex -> { - log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - socket.closeHandler(v -> { - log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - cleanupConnection(socket); - }); - - // 2. 设置文本消息处理器 - socket.textMessageHandler(message -> { - try { - processMessage(clientId, message, socket); - } catch (Exception e) { - log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", - clientId, socket.remoteAddress(), e.getMessage()); - cleanupConnection(socket); - socket.close(); - } - }); - } - - /** - * 处理消息 - * - * @param clientId 客户端 ID - * @param message 消息(JSON 字符串) - * @param socket WebSocket 连接 - * @throws Exception 消息解码失败时抛出异常 - */ - private void processMessage(String clientId, String message, ServerWebSocket socket) throws Exception { - // 1.1 基础检查 - if (StrUtil.isBlank(message)) { - return; - } - // 1.2 解码消息(已认证连接使用其 codecType,未认证连接使用默认 CODEC_TYPE) - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - String codecType = connectionInfo != null ? connectionInfo.getCodecType() : CODEC_TYPE; - IotDeviceMessage deviceMessage; - try { - deviceMessage = deviceMessageService.decodeDeviceMessage( - StrUtil.utf8Bytes(message), codecType); - if (deviceMessage == null) { - throw new Exception("解码后消息为空"); - } - } catch (Exception e) { - throw new Exception("消息解码失败: " + e.getMessage(), e); - } - - // 2. 根据消息类型路由处理 - try { - if (AUTH_METHOD.equals(deviceMessage.getMethod())) { - // 认证请求 - handleAuthenticationRequest(clientId, deviceMessage, socket); - } else if (IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod().equals(deviceMessage.getMethod())) { - // 设备动态注册请求 - handleRegisterRequest(clientId, deviceMessage, socket); - } else { - // 业务消息 - handleBusinessRequest(clientId, deviceMessage, socket); - } - } catch (Exception e) { - log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", - clientId, deviceMessage.getMethod(), e); - // 发送错误响应,避免客户端一直等待 - try { - sendErrorResponse(socket, deviceMessage.getRequestId(), "消息处理失败"); - } catch (Exception responseEx) { - log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); - } - } - } - - /** - * 处理认证请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param socket WebSocket 连接 - */ - private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1.1 解析认证参数 - IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); - if (authParams == null) { - log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "认证参数不完整"); - return; - } - // 1.2 执行认证 - if (!validateDeviceAuth(authParams)) { - log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", - clientId, authParams.getUsername()); - sendErrorResponse(socket, message.getRequestId(), "认证失败"); - return; - } - - // 2.1 解析设备信息 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); - if (deviceInfo == null) { - sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败"); - return; - } - // 2.2 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendErrorResponse(socket, message.getRequestId(), "设备不存在"); - return; - } - - // 3.1 注册连接 - registerConnection(socket, device, clientId); - // 3.2 发送上线消息 - sendOnlineMessage(device); - // 3.3 发送成功响应 - sendSuccessResponse(socket, message.getRequestId(), "认证成功"); - log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", - device.getId(), device.getDeviceName()); - } catch (Exception e) { - log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "认证处理异常"); - } - } - - /** - * 处理设备动态注册请求(一型一密,不需要认证) - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param socket WebSocket 连接 - * @see 阿里云 - 一型一密 - */ - private void handleRegisterRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1. 解析注册参数 - IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams()); - if (params == null - || StrUtil.hasEmpty(params.getProductKey(), params.getDeviceName(), params.getProductSecret())) { - log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "注册参数不完整"); - return; - } - - // 2. 调用动态注册 - CommonResult result = deviceApi.registerDevice(params); - if (result.isError()) { - log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg()); - sendErrorResponse(socket, message.getRequestId(), result.getMsg()); - return; - } - - // 3. 发送成功响应(包含 deviceSecret) - sendRegisterSuccessResponse(socket, message.getRequestId(), result.getData()); - log.info("[handleRegisterRequest][注册成功,客户端 ID: {},设备名: {}]", - clientId, params.getDeviceName()); - } catch (Exception e) { - log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e); - sendErrorResponse(socket, message.getRequestId(), "注册处理异常"); - } - } - - /** - * 处理业务请求 - * - * @param clientId 客户端 ID - * @param message 消息信息 - * @param socket WebSocket 连接 - */ - private void handleBusinessRequest(String clientId, IotDeviceMessage message, ServerWebSocket socket) { - try { - // 1. 获取认证信息并处理业务消息 - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo == null) { - log.warn("[handleBusinessRequest][连接未认证,拒绝处理业务消息,客户端 ID: {}]", clientId); - sendErrorResponse(socket, message.getRequestId(), "连接未认证"); - return; - } - - // 2. 发送消息到消息总线 - deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", - clientId, message.toString()); - } catch (Exception e) { - log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); - } - } - - /** - * 注册连接信息 - * - * @param socket WebSocket 连接 - * @param device 设备 - * @param clientId 客户端 ID - */ - private void registerConnection(ServerWebSocket socket, IotDeviceRespDTO device, String clientId) { - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = new IotWebSocketConnectionManager.ConnectionInfo() - .setDeviceId(device.getId()) - .setProductKey(device.getProductKey()) - .setDeviceName(device.getDeviceName()) - .setClientId(clientId) - .setCodecType(CODEC_TYPE); - // 注册连接 - connectionManager.registerConnection(socket, device.getId(), connectionInfo); - } - - /** - * 发送设备上线消息 - * - * @param device 设备信息 - */ - private void sendOnlineMessage(IotDeviceRespDTO device) { - try { - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), - device.getDeviceName(), serverId); - } catch (Exception e) { - log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); - } - } - - /** - * 清理连接 - * - * @param socket WebSocket 连接 - */ - private void cleanupConnection(ServerWebSocket socket) { - try { - // 1. 发送离线消息(如果已认证) - IotWebSocketConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); - if (connectionInfo != null) { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), - connectionInfo.getDeviceName(), serverId); - } - - // 2. 注销连接 - connectionManager.unregisterConnection(socket); - } catch (Exception e) { - log.error("[cleanupConnection][清理连接失败]", e); - } - } - - /** - * 发送响应消息 - * - * @param socket WebSocket 连接 - * @param success 是否成功 - * @param message 消息 - * @param requestId 请求 ID - */ - private void sendResponse(ServerWebSocket socket, boolean success, String message, String requestId) { - try { - Object responseData = MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - - int code = success ? 0 : 401; - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, code, message); - - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); - socket.writeTextMessage(StrUtil.utf8Str(encodedData)); - } catch (Exception e) { - log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); - } - } - - /** - * 验证设备认证信息 - * - * @param authParams 认证参数 - * @return 是否认证成功 - */ - private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { - try { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) - .setPassword(authParams.getPassword())); - result.checkError(); - return BooleanUtil.isTrue(result.getData()); - } catch (Exception e) { - log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); - return false; - } - } - - /** - * 发送错误响应 - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param errorMessage 错误消息 - */ - private void sendErrorResponse(ServerWebSocket socket, String requestId, String errorMessage) { - sendResponse(socket, false, errorMessage, requestId); - } - - /** - * 发送成功响应 - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param message 消息 - */ - @SuppressWarnings("SameParameterValue") - private void sendSuccessResponse(ServerWebSocket socket, String requestId, String message) { - sendResponse(socket, true, message, requestId); - } - - /** - * 解析认证参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 认证参数 DTO,解析失败时返回 null - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - private IotDeviceAuthReqDTO parseAuthParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceAuthReqDTO() - .setClientId(MapUtil.getStr(paramMap, "clientId")) - .setUsername(MapUtil.getStr(paramMap, "username")) - .setPassword(MapUtil.getStr(paramMap, "password")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceAuthReqDTO) { - return (IotDeviceAuthReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); - } catch (Exception e) { - log.error("[parseAuthParams][解析认证参数({})失败]", params, e); - return null; - } - } - - /** - * 解析注册参数 - * - * @param params 参数对象(通常为 Map 类型) - * @return 注册参数 DTO,解析失败时返回 null - */ - @SuppressWarnings("unchecked") - private IotDeviceRegisterReqDTO parseRegisterParams(Object params) { - if (params == null) { - return null; - } - try { - // 参数默认为 Map 类型,直接转换 - if (params instanceof Map) { - Map paramMap = (Map) params; - return new IotDeviceRegisterReqDTO() - .setProductKey(MapUtil.getStr(paramMap, "productKey")) - .setDeviceName(MapUtil.getStr(paramMap, "deviceName")) - .setProductSecret(MapUtil.getStr(paramMap, "productSecret")); - } - // 如果已经是目标类型,直接返回 - if (params instanceof IotDeviceRegisterReqDTO) { - return (IotDeviceRegisterReqDTO) params; - } - - // 其他情况尝试 JSON 转换 - return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class); - } catch (Exception e) { - log.error("[parseRegisterParams][解析注册参数({})失败]", params, e); - return null; - } - } - - /** - * 发送注册成功响应(包含 deviceSecret) - * - * @param socket WebSocket 连接 - * @param requestId 请求 ID - * @param registerResp 注册响应 - */ - private void sendRegisterSuccessResponse(ServerWebSocket socket, String requestId, - IotDeviceRegisterRespDTO registerResp) { - try { - // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO) - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null); - // 2. 发送响应 - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, CODEC_TYPE); - socket.writeTextMessage(StrUtil.utf8Str(encodedData)); - } catch (Exception e) { - log.error("[sendRegisterSuccessResponse][发送注册成功响应失败,requestId: {}]", requestId, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java new file mode 100644 index 000000000..095dae0b6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.gateway.serialize; + +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; + +/** + * IoT 设备消息序列化器接口 + * + * 用于序列化和反序列化设备消息 + * + * @author 芋道源码 + */ +public interface IotMessageSerializer { + + /** + * 序列化消息 + * + * @param message 消息 + * @return 编码后的消息内容 + */ + byte[] serialize(IotDeviceMessage message); + + /** + * 反序列化消息 + * + * @param bytes 消息内容 + * @return 解码后的消息内容 + */ + IotDeviceMessage deserialize(byte[] bytes); + + /** + * 获取序列化类型 + * + * @return 序列化类型枚举 + */ + IotSerializeTypeEnum getType(); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java new file mode 100644 index 000000000..0c072a5a1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializerManager.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.gateway.serialize; + +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.serialize.binary.IotBinarySerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import lombok.extern.slf4j.Slf4j; + +import java.util.EnumMap; +import java.util.Map; + +/** + * IoT 序列化器管理器 + * + * 负责根据枚举创建和管理序列化器实例 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMessageSerializerManager { + + private final Map serializerMap = new EnumMap<>(IotSerializeTypeEnum.class); + + public IotMessageSerializerManager() { + // 遍历枚举,创建对应的序列化器 + for (IotSerializeTypeEnum type : IotSerializeTypeEnum.values()) { + IotMessageSerializer serializer = createSerializer(type); + serializerMap.put(type, serializer); + log.info("[IotSerializerManager][序列化器 {} 创建成功]", type); + } + } + + /** + * 根据类型创建序列化器 + * + * @param type 序列化类型 + * @return 序列化器实例 + */ + @SuppressWarnings("EnhancedSwitchMigration") + private IotMessageSerializer createSerializer(IotSerializeTypeEnum type) { + switch (type) { + case JSON: + return new IotJsonSerializer(); + case BINARY: + return new IotBinarySerializer(); + default: + throw new IllegalArgumentException("未知的序列化类型:" + type); + } + } + + /** + * 获取序列化器 + * + * @param type 序列化类型 + * @return 序列化器实例 + */ + public IotMessageSerializer get(IotSerializeTypeEnum type) { + return serializerMap.get(type); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/binary/IotBinarySerializer.java similarity index 81% rename from yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java rename to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/binary/IotBinarySerializer.java index 05098cccb..7227c4d7f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/binary/IotBinarySerializer.java @@ -1,20 +1,20 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; +package cn.iocoder.yudao.module.iot.gateway.serialize.binary; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import io.vertx.core.buffer.Buffer; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; /** - * TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器 - *

+ * 二进制格式的消息序列化器 + * * 二进制协议格式(所有数值使用大端序): * *

@@ -28,20 +28,15 @@ import java.nio.charset.StandardCharsets;
  * |                        消息体数据(变长)                              |
  * +--------+--------+--------+--------+--------+--------+--------+--------+
  * 
- *

+ * * 消息体格式: * - 请求消息:params 数据(JSON) * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON) - *

- * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 * * @author 芋道源码 */ @Slf4j -@Component -public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { - - public static final String TYPE = "TCP_BINARY"; +public class IotBinarySerializer implements IotMessageSerializer { /** * 协议魔术字,用于协议识别 @@ -74,12 +69,12 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; @Override - public String type() { - return TYPE; + public IotSerializeTypeEnum getType() { + return IotSerializeTypeEnum.BINARY; } @Override - public byte[] encode(IotDeviceMessage message) { + public byte[] serialize(IotDeviceMessage message) { Assert.notNull(message, "消息不能为空"); Assert.notBlank(message.getMethod(), "消息方法不能为空"); try { @@ -90,19 +85,19 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { // 3. 构建完整消息 return buildCompleteMessage(message, messageType, bodyData); } catch (Exception e) { - log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e); - throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e); + log.error("[encode][二进制消息编码失败,消息: {}]", message, e); + throw new RuntimeException("二进制消息编码失败: " + e.getMessage(), e); } } @Override - public IotDeviceMessage decode(byte[] bytes) { + public IotDeviceMessage deserialize(byte[] bytes) { Assert.notNull(bytes, "待解码数据不能为空"); Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); try { Buffer buffer = Buffer.buffer(bytes); - // 解析协议头部和消息内容 int index = 0; + // 1. 验证魔术字 byte magic = buffer.getByte(index++); Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic); @@ -113,9 +108,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { // 3. 读取消息类型 byte messageType = buffer.getByte(index++); - // 直接验证消息类型,无需抽取方法 - Assert.isTrue(messageType == REQUEST || messageType == RESPONSE, - "无效的消息类型: " + messageType); + Assert.isTrue(messageType == REQUEST || messageType == RESPONSE, "无效的消息类型: " + messageType); // 4. 读取消息长度 int messageLength = buffer.getInt(index); @@ -138,27 +131,28 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { // 7. 解析消息体 return parseMessageBody(buffer, index, messageType, messageId, method); } catch (Exception e) { - log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e); - throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e); + log.error("[decode][二进制消息解码失败,数据长度: {}]", bytes.length, e); + throw new RuntimeException("二进制消息解码失败: " + e.getMessage(), e); } } /** - * 确定消息类型 - * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息 + * 快速检测是否为二进制格式 + * + * @param data 数据 + * @return 是否为二进制格式 */ + public static boolean isBinaryFormat(byte[] data) { + return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER; + } + private byte determineMessageType(IotDeviceMessage message) { - // 判断是否为响应消息:有响应码或响应消息时为响应 if (message.getCode() != null) { return RESPONSE; } - // 默认为请求消息 return REQUEST; } - /** - * 构建消息体 - */ private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) { Buffer bodyBuffer = Buffer.buffer(); if (messageType == RESPONSE) { @@ -175,7 +169,6 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { } } else { // 请求消息只处理 params 参数 - // TODO @haohao:如果为空,是不是得写个长度 0 哈? if (message.getParams() != null) { bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getParams())); } @@ -183,16 +176,13 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { return bodyBuffer.getBytes(); } - /** - * 构建完整消息 - */ private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) { Buffer buffer = Buffer.buffer(); // 1. 写入协议头部 buffer.appendByte(MAGIC_NUMBER); buffer.appendByte(PROTOCOL_VERSION); buffer.appendByte(messageType); - // 2. 预留消息长度位置(在 5. 更新消息长度) + // 2. 预留消息长度位置 int lengthPosition = buffer.length(); buffer.appendInt(0); // 3. 写入消息 ID @@ -212,29 +202,20 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { return buffer.getBytes(); } - /** - * 解析消息体 - */ private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType, String messageId, String method) { if (startIndex >= buffer.length()) { - // 空消息体 return IotDeviceMessage.of(messageId, method, null, null, null, null); } if (messageType == RESPONSE) { - // 响应消息:解析 code + msg + data return parseResponseMessage(buffer, startIndex, messageId, method); } else { - // 请求消息:解析 payload Object payload = parseJsonData(buffer, startIndex, buffer.length()); return IotDeviceMessage.of(messageId, method, payload, null, null, null); } } - /** - * 解析响应消息 - */ private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) { int index = startIndex; @@ -257,9 +238,6 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { return IotDeviceMessage.of(messageId, method, null, data, code, msg); } - /** - * 解析 JSON 数据 - */ private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { if (startIndex >= endIndex) { return null; @@ -273,14 +251,4 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { } } - /** - * 快速检测是否为二进制格式 - * - * @param data 数据 - * @return 是否为二进制格式 - */ - public static boolean isBinaryFormatQuick(byte[] data) { - return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER; - } - -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java new file mode 100644 index 000000000..7fa657075 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.gateway.serialize.json; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; + +/** + * JSON 格式的消息序列化器 + * + * 直接使用 JsonUtils 序列化/反序列化 {@link IotDeviceMessage},不包装额外字段 + * + * @author 芋道源码 + */ +public class IotJsonSerializer implements IotMessageSerializer { + + @Override + public IotSerializeTypeEnum getType() { + return IotSerializeTypeEnum.JSON; + } + + @Override + public byte[] serialize(IotDeviceMessage message) { + Assert.notNull(message, "消息不能为空"); + return JsonUtils.toJsonByte(message); + } + + @Override + public IotDeviceMessage deserialize(byte[] bytes) { + Assert.notNull(bytes, "待解码数据不能为空"); + IotDeviceMessage message = JsonUtils.parseObject(bytes, IotDeviceMessage.class); + Assert.notNull(message, "消息解码失败"); + return message; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java new file mode 100644 index 000000000..cfdda5ac0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/package-info.java @@ -0,0 +1,6 @@ +/** + * 消息序列化:将设备消息转换为字节数组(JSON、二进制等格式) + * + * @see cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer + */ +package cn.iocoder.yudao.module.iot.gateway.serialize; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java index c86fc0983..7d16a655c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; /** @@ -10,45 +11,45 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; public interface IotDeviceMessageService { /** - * 编码消息 + * 序列化消息 * * @param message 消息 * @param productKey 产品 Key * @param deviceName 设备名称 - * @return 编码后的消息内容 + * @return 序列化后的消息内容 */ - byte[] encodeDeviceMessage(IotDeviceMessage message, - String productKey, String deviceName); + byte[] serializeDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName); /** - * 编码消息 + * 序列化消息 * - * @param message 消息 - * @param codecType 编解码器类型 - * @return 编码后的消息内容 + * @param message 消息 + * @param serializeType 序列化类型 + * @return 序列化后的消息内容 */ - byte[] encodeDeviceMessage(IotDeviceMessage message, - String codecType); + byte[] serializeDeviceMessage(IotDeviceMessage message, + IotSerializeTypeEnum serializeType); /** - * 解码消息 + * 反序列化消息 * * @param bytes 消息内容 * @param productKey 产品 Key * @param deviceName 设备名称 - * @return 解码后的消息内容 + * @return 反序列化后的消息内容 */ - IotDeviceMessage decodeDeviceMessage(byte[] bytes, - String productKey, String deviceName); + IotDeviceMessage deserializeDeviceMessage(byte[] bytes, + String productKey, String deviceName); /** - * 解码消息 + * 反序列化消息 * - * @param bytes 消息内容 - * @param codecType 编解码器类型 - * @return 解码后的消息内容 + * @param bytes 消息内容 + * @param serializeType 序列化类型 + * @return 反序列化后的消息内容 */ - IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType); + IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType); /** * 发送消息 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java index 014da9a5d..ee0c4aea4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java @@ -1,20 +1,20 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS; @@ -28,80 +28,70 @@ import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVIC @Slf4j public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { - /** - * 编解码器 - */ - private final Map codes; - @Resource private IotDeviceService deviceService; @Resource private IotDeviceMessageProducer deviceMessageProducer; - public IotDeviceMessageServiceImpl(List codes) { - this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type); - } + @Resource + private IotMessageSerializerManager messageSerializerManager; @Override - public byte[] encodeDeviceMessage(IotDeviceMessage message, - String productKey, String deviceName) { + public byte[] serializeDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName) { // 1.1 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); if (device == null) { throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); } - // 1.2 获取编解码器 - IotDeviceMessageCodec codec = codes.get(device.getCodecType()); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); - } + // 1.2 获取序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType()); + Assert.notNull(serializeType, "设备序列化类型不能为空"); - // 2. 编码消息 - return codec.encode(message); + // 2. 序列化消息 + return serializeDeviceMessage(message, serializeType); } @Override - public byte[] encodeDeviceMessage(IotDeviceMessage message, - String codecType) { - // 1. 获取编解码器 - IotDeviceMessageCodec codec = codes.get(codecType); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + public byte[] serializeDeviceMessage(IotDeviceMessage message, + IotSerializeTypeEnum serializeType) { + // 1. 获取序列化器 + IotMessageSerializer serializer = messageSerializerManager.get(serializeType); + if (serializer == null) { + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType)); } - // 2. 编码消息 - return codec.encode(message); + // 2. 序列化消息 + return serializer.serialize(message); } @Override - public IotDeviceMessage decodeDeviceMessage(byte[] bytes, - String productKey, String deviceName) { + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, + String productKey, String deviceName) { // 1.1 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); if (device == null) { throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); } - // 1.2 获取编解码器 - IotDeviceMessageCodec codec = codes.get(device.getCodecType()); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); - } + // 1.2 获取序列化器 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType()); + Assert.notNull(serializeType, "设备序列化类型不能为空"); - // 2. 解码消息 - return codec.decode(bytes); + // 2. 反序列化消息 + return deserializeDeviceMessage(bytes, serializeType); } @Override - public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) { - // 1. 获取编解码器 - IotDeviceMessageCodec codec = codes.get(codecType); - if (codec == null) { - throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, IotSerializeTypeEnum serializeType) { + // 1. 获取序列化器 + IotMessageSerializer serializer = messageSerializerManager.get(serializeType); + if (serializer == null) { + throw new IllegalArgumentException(StrUtil.format("序列化器({}) 不存在", serializeType)); } - // 2. 解码消息 - return codec.decode(bytes); + // 2. 反序列化消息 + return serializer.deserialize(bytes); } @Override diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java index 3c4180fc5..85679eaf6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java @@ -3,10 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.remote; import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.*; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; @@ -44,7 +41,7 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { public void init() { IotGatewayProperties.RpcProperties rpc = gatewayProperties.getRpc(); restTemplate = new RestTemplateBuilder() - .rootUri(rpc.getUrl() + "/rpc-api/iot/device") + .rootUri(rpc.getUrl()) .readTimeout(rpc.getReadTimeout()) .connectTimeout(rpc.getConnectTimeout()) .build(); @@ -52,12 +49,17 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { @Override public CommonResult authDevice(IotDeviceAuthReqDTO authReqDTO) { - return doPost("/auth", authReqDTO, new ParameterizedTypeReference<>() { }); + return doPost("/rpc-api/iot/device/auth", authReqDTO, new ParameterizedTypeReference<>() { }); } @Override public CommonResult getDevice(IotDeviceGetReqDTO getReqDTO) { - return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); + return doPost("/rpc-api/iot/device/get", getReqDTO, new ParameterizedTypeReference<>() { }); + } + + @Override + public CommonResult> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) { + return doPost("/rpc-api/iot/modbus/config-list", listReqDTO, new ParameterizedTypeReference<>() { }); } @Override diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java index 7f72937ef..60e8d6c7b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.iot.gateway.util; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; /** * IoT 网关 MQTT 主题工具类 @@ -38,6 +40,38 @@ public final class IotMqttTopicUtils { */ public static final String MQTT_EVENT_PATH = "/mqtt/event"; + /** + * MQTT ACL 接口路径 + * 对应 EMQX HTTP ACL 插件的 ACL 请求接口 + */ + public static final String MQTT_ACL_PATH = "/mqtt/acl"; + + // ========== 消息方法标准化 ========== + + /** + * 标准化设备回复消息的 method + *

+ * MQTT 协议中,设备回复下行指令时,topic 和 method 会携带 _reply 后缀 + * (如 thing.service.invoke_reply)。平台内部统一使用基础 method(如 thing.service.invoke), + * 通过 {@link IotDeviceMessage#getCode()} 非空来识别回复消息。 + *

+ * 此方法剥离 _reply 后缀,并确保 code 字段被设置。 + * + * @param message 设备消息 + */ + public static void normalizeReplyMethod(IotDeviceMessage message) { + String method = message.getMethod(); + if (!StrUtil.endWith(method, REPLY_TOPIC_SUFFIX)) { + return; + } + // 1. 剥离 _reply 后缀 + message.setMethod(method.substring(0, method.length() - REPLY_TOPIC_SUFFIX.length())); + // 2. 确保 code 被设置,使 isReplyMessage() 能正确识别 + if (message.getCode() == null) { + message.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + } + } + // ========== 工具方法 ========== /** @@ -63,4 +97,48 @@ public final class IotMqttTopicUtils { return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; } -} \ No newline at end of file + /** + * 校验主题是否允许订阅 + *

+ * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头, + * 或者是通配符形式 /sys/{productKey}/{deviceName}/# + * + * @param topic 订阅的主题 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 是否允许订阅 + */ + public static boolean isTopicSubscribeAllowed(String topic, String productKey, String deviceName) { + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { + return false; + } + // 构建设备主题前缀 + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; + // 主题必须以设备前缀开头,或者是设备前缀的通配符形式 + return topic.startsWith(deviceTopicPrefix) + || topic.equals(SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/#"); + } + + /** + * 校验主题是否允许发布 + *

+ * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头,且不允许包含通配符(+/#)。 + * + * @param topic 发布的主题 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 是否允许发布 + */ + public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) { + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { + return false; + } + // MQTT publish topic 不允许包含通配符,但这里做一次兜底校验 + if (topic.contains("#") || topic.contains("+")) { + return false; + } + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; + return topic.startsWith(deviceTopicPrefix); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 45216962c..18b178986 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -42,98 +42,148 @@ yudao: secret: yudaoIotGatewayTokenSecret123456789 # Token 密钥,至少32位 expiration: 7d - # 协议配置 - protocol: + # 协议实例列表 + protocols: # ==================================== # 针对引入的 HTTP 组件的配置 # ==================================== - http: - enabled: true - server-port: 8092 - # ==================================== - # 针对引入的 EMQX 组件的配置 - # ==================================== - emqx: + - id: http-json + protocol: http + port: 8092 enabled: false - http-port: 8090 # MQTT HTTP 服务端口 - mqtt-host: 127.0.0.1 # MQTT Broker 地址 - mqtt-port: 1883 # MQTT Broker 端口 - mqtt-username: admin # MQTT 用户名 - mqtt-password: public # MQTT 密码 - mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID - mqtt-ssl: false # 是否开启 SSL - mqtt-topics: - - "/sys/#" # 系统主题 - clean-session: true # 是否启用 Clean Session (默认: true) - keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60) - max-inflight-queue: 10000 # 最大飞行消息队列,单位:条 - connect-timeout-seconds: 10 # 连接超时,单位:秒 - # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false! - # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true - trust-all: true # 在 dev 环境可以设为 true - # 遗嘱消息配置 (用于网关异常下线时通知其他系统) - will: - enabled: true # 生产环境强烈建议开启 - topic: "gateway/status/${yudao.iot.gateway.emqx.mqtt-client-id}" # 遗嘱消息主题 - payload: "offline" # 遗嘱消息负载 - qos: 1 # 遗嘱消息 QoS - retain: true # 遗嘱消息是否保留 - # 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效) - ssl-options: - key-store-path: "classpath:certs/client.jks" # 客户端证书库路径 - key-store-password: "your-keystore-password" # 客户端证书库密码 - trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 - trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 # ==================================== # 针对引入的 TCP 组件的配置 # ==================================== - tcp: + - id: tcp-json enabled: false + protocol: tcp port: 8091 - keep-alive-timeout-ms: 30000 - max-connections: 1000 - ssl-enabled: false - ssl-cert-path: "classpath:certs/client.jks" - ssl-key-path: "classpath:certs/client.jks" + serialize: json + tcp: + max-connections: 1000 + keep-alive-timeout-ms: 30000 + codec: + type: delimiter # 拆包类型:length_field / delimiter / fixed_length + delimiter: "\\n" # 分隔符(支持转义:\\n=换行, \\r=回车, \\t=制表符) +# type: length_field # 拆包类型:length_field / delimiter / fixed_length +# length-field-offset: 0 # 长度字段偏移量 +# length-field-length: 4 # 长度字段长度 +# length-adjustment: 0 # 长度调整值 +# initial-bytes-to-strip: 4 # 初始跳过的字节数 +# type: fixed_length # 拆包类型:length_field / delimiter / fixed_length +# fixed-length: 256 # 固定长度 # ==================================== # 针对引入的 UDP 组件的配置 # ==================================== - udp: - enabled: false # 是否启用 UDP - port: 8093 # UDP 服务端口 - receive-buffer-size: 65536 # 接收缓冲区大小(字节,默认 64KB) - send-buffer-size: 65536 # 发送缓冲区大小(字节,默认 64KB) - session-timeout-ms: 60000 # 会话超时时间(毫秒,默认 60 秒) - session-clean-interval-ms: 30000 # 会话清理间隔(毫秒,默认 30 秒) - # ==================================== - # 针对引入的 MQTT 组件的配置 - # ==================================== - mqtt: + - id: udp-json enabled: false - port: 1883 - max-message-size: 8192 - connect-timeout-seconds: 60 - ssl-enabled: false - # ==================================== - # 针对引入的 CoAP 组件的配置 - # ==================================== - coap: - enabled: false # 是否启用 CoAP 协议 - port: 5683 # CoAP 服务端口(默认 5683) - max-message-size: 1024 # 最大消息大小(字节) - ack-timeout: 2000 # ACK 超时时间(毫秒) - max-retransmit: 4 # 最大重传次数 + protocol: udp + port: 8093 + serialize: json + udp: + max-sessions: 1000 # 最大会话数 + session-timeout-ms: 60000 # 会话超时时间(毫秒),基于 Guava Cache 自动过期 + receive-buffer-size: 65536 # 接收缓冲区大小(字节) + send-buffer-size: 65536 # 发送缓冲区大小(字节) # ==================================== # 针对引入的 WebSocket 组件的配置 # ==================================== - websocket: - enabled: false # 是否启用 WebSocket 协议 - port: 8094 # WebSocket 服务端口(默认 8094) - path: /ws # WebSocket 路径(默认 /ws) - max-message-size: 65536 # 最大消息大小(字节,默认 64KB) - max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) - idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) - ssl-enabled: false # 是否启用 SSL(wss://) + - id: websocket-json + enabled: false + protocol: websocket + port: 8094 + serialize: json + websocket: + path: /ws + max-message-size: 65536 # 最大消息大小(字节,默认 64KB) + max-frame-size: 65536 # 最大帧大小(字节,默认 64KB) + idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60) + # ==================================== + # 针对引入的 CoAP 组件的配置 + # ==================================== + - id: coap-json + enabled: false + protocol: coap + port: 5683 + coap: + max-message-size: 1024 # 最大消息大小(字节) + ack-timeout-ms: 2000 # ACK 超时时间(毫秒) + max-retransmit: 4 # 最大重传次数 + # ==================================== + # 针对引入的 MQTT 组件的配置 + # ==================================== + - id: mqtt-json + enabled: false + protocol: mqtt + port: 1883 + serialize: json + mqtt: + max-message-size: 8192 # 最大消息大小(字节) + connect-timeout-seconds: 60 # 连接超时时间(秒) + # ==================================== + # 针对引入的 EMQX 组件的配置 + # ==================================== + - id: emqx-1 + enabled: false + protocol: emqx + port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event) + emqx: + mqtt-host: 127.0.0.1 # MQTT Broker 地址 + mqtt-port: 1883 # MQTT Broker 端口 + mqtt-username: admin # MQTT 用户名 + mqtt-password: public # MQTT 密码 + mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID + mqtt-ssl: false # 是否开启 SSL + mqtt-topics: + - "/sys/#" # 系统主题 + mqtt-qos: 1 # 默认 QoS + clean-session: true # 是否启用 Clean Session (默认: true) + keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60) + max-inflight-queue: 10000 # 最大飞行消息队列,单位:条 + connect-timeout-seconds: 10 # 连接超时,单位:秒 + reconnect-delay-ms: 5000 # 重连延迟,单位:毫秒 + # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false! + # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true + trust-all: true # 在 dev 环境可以设为 true + # EMQX HTTP Hook 回调网关的 HTTPS 配置(可选) + http: + ssl-enabled: false + # ssl-cert-path: "path/to/server.crt" + # ssl-key-path: "path/to/server.key" + # 遗嘱消息配置 (用于网关异常下线时通知其他系统) + will: + enabled: true # 生产环境强烈建议开启 + topic: "gateway/status/iot-gateway-mqtt" # 遗嘱消息主题 + payload: "offline" # 遗嘱消息负载 + qos: 1 # 遗嘱消息 QoS + retain: true # 遗嘱消息是否保留 + # 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效) + ssl-options: + key-store-path: "classpath:certs/client.jks" # 客户端证书库路径 + key-store-password: "your-keystore-password" # 客户端证书库密码 + trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 + trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 + # ==================================== + # 针对引入的 Modbus TCP Client 组件的配置 + # ==================================== + - id: modbus-tcp-client-1 + enabled: false + protocol: modbus_tcp_client + port: 502 + modbus-tcp-client: + config-refresh-interval: 30 # 配置刷新间隔(秒) + # ==================================== + # 针对引入的 Modbus TCP Server 组件的配置 + # ==================================== + - id: modbus-tcp-server-1 + enabled: false + protocol: modbus_tcp_server + port: 503 + modbus-tcp-server: + config-refresh-interval: 30 # 配置刷新间隔(秒) + custom-function-code: 65 # 自定义功能码(用于认证等扩展交互) + request-timeout: 5000 # Pending Request 超时时间(毫秒) + request-cleanup-interval: 10000 # Pending Request 清理间隔(毫秒) --- #################### 日志相关配置 #################### @@ -153,9 +203,9 @@ logging: cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG - cn.iocoder.yudao.module.iot.gateway.protocol.mqttws: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG + cn.iocoder.yudao.module.iot.gateway.protocol.modbus: DEBUG # 根日志级别 root: INFO diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java index 583763e22..8fc49901f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java @@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -23,6 +23,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN; + /** * IoT 直连设备 CoAP 协议集成测试(手动测试) * @@ -134,7 +136,7 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -177,7 +179,7 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -202,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest { // 1.1 构建请求 String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); // 1.2 构建请求参数 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); - reqDTO.setProductKey(PRODUCT_KEY); - reqDTO.setDeviceName("test-" + System.currentTimeMillis()); - reqDTO.setProductSecret("test-product-secret"); + String deviceName = "test-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName(deviceName) + .setSign(sign); String payload = JsonUtils.toJsonString(reqDTO); // 1.3 输出请求 log.info("[testDeviceRegister][请求 URI: {}]", uri); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java index ca581cb96..f350325dd 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewayDeviceCoapProtocolIntegrationTest.java @@ -13,7 +13,6 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -30,6 +29,8 @@ import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.Map; +import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN; + /** * IoT 网关设备 CoAP 协议集成测试(手动测试) * @@ -158,7 +159,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -201,7 +202,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -242,7 +243,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -289,7 +290,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -362,7 +363,7 @@ public class IotGatewayDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, GATEWAY_TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, GATEWAY_TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java index 7aed8ecb6..4d909a2d2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotGatewaySubDeviceCoapProtocolIntegrationTest.java @@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; @@ -22,6 +21,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.IotCoapAbstractHandler.OPTION_TOKEN; + /** * IoT 网关子设备 CoAP 协议集成测试(手动测试) * @@ -137,7 +138,7 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 @@ -185,7 +186,7 @@ public class IotGatewaySubDeviceCoapProtocolIntegrationTest { request.setURI(uri); request.setPayload(payload); request.getOptions().setContentFormat(MediaTypeRegistry.APPLICATION_JSON); - request.getOptions().addOption(new Option(IotCoapUtils.OPTION_TOKEN, TOKEN)); + request.getOptions().addOption(new Option(OPTION_TOKEN, TOKEN)); CoapResponse response = client.advanced(request); // 2.2 输出结果 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java new file mode 100644 index 000000000..d7d453545 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java @@ -0,0 +1,18 @@ +/** + * IoT 网关 EMQX 协议集成测试包 + * + *

+ * 测试类直接使用 mqtt 包下的单测即可,因为设备都是通过 MQTT 协议连接 EMQX Broker。 + * + * @see cn.iocoder.yudao.module.iot.gateway.protocol.mqtt + * + *

架构

+ *
+ * +--------+      MQTT       +-------------+     HTTP Hook     +---------+
+ * |  设备  | --------------> | EMQX Broker | ----------------> |  网关   |
+ * +--------+                 +-------------+                   +---------+
+ * 
+ * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java index 8dd36cc63..1759000b0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -11,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -92,9 +92,7 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("width", 1) .put("height", "2") @@ -126,9 +124,7 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") .put("params", IotDeviceEventPostReqDTO.of( "eat", MapUtil.builder().put("rice", 3).build(), @@ -163,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest { // 1.1 构建请求 String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); // 1.2 构建请求参数 - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); - reqDTO.setProductKey(PRODUCT_KEY); - reqDTO.setDeviceName("test-" + System.currentTimeMillis()); - reqDTO.setProductSecret("test-product-secret"); + String deviceName = "test-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName(deviceName) + .setSign(sign); String payload = JsonUtils.toJsonString(reqDTO); // 1.3 输出请求 log.info("[testDeviceRegister][请求 URL: {}]", url); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java index 354c4d685..779c588b7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -20,7 +19,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.Collections; -import java.util.List; import java.util.Map; @@ -121,9 +119,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); params.setSubDevices(Collections.singletonList(subDeviceAuth)); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.TOPO_ADD.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.4 输出请求 @@ -155,9 +151,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { params.setSubDevices(Collections.singletonList( new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.3 输出请求 @@ -187,9 +181,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { // 1.2 构建请求参数(目前为空,预留扩展) IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.TOPO_GET.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.3 输出请求 @@ -208,8 +200,6 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { // ===================== 子设备注册测试 ===================== - // TODO @芋艿:待测试 - /** * 子设备动态注册测试 *

@@ -227,9 +217,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); subDevice.setDeviceName("mougezishebei"); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod()) - .put("version", "1.0") .put("params", Collections.singletonList(subDevice)) .build()); // 1.3 输出请求 @@ -263,9 +251,9 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { .put("temperature", 25.5) .build(); // 1.3 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("message", "gateway started").build()) + .setTime(System.currentTimeMillis()); Map gatewayEvents = MapUtil.builder() .put("statusReport", gatewayEvent) .build(); @@ -274,26 +262,24 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { .put("power", 100) .build(); // 1.5 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("errorCode", 0).build()) + .setTime(System.currentTimeMillis()); Map subDeviceEvents = MapUtil.builder() .put("healthCheck", subDeviceEvent) .build(); // 1.6 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) + .setProperties(subDeviceProperties) + .setEvents(subDeviceEvents); // 1.7 构建请求参数 IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); params.setProperties(gatewayProperties); params.setEvents(gatewayEvents); params.setSubDevices(ListUtil.of(subDeviceData)); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod()) - .put("version", "1.0") .put("params", params) .build()); // 1.8 输出请求 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java index cfebdbe3f..f6b9399bc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewaySubDeviceHttpProtocolIntegrationTest.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -94,9 +93,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/property/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) - .put("version", "1.0") .put("params", IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("power", 100) .put("status", "online") @@ -130,9 +127,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest { String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/event/post", SERVER_HOST, SERVER_PORT, PRODUCT_KEY, DEVICE_NAME); String payload = JsonUtils.toJsonString(MapUtil.builder() - .put("id", IdUtil.fastSimpleUUID()) .put("method", IotDeviceMessageMethodEnum.EVENT_POST.getMethod()) - .put("version", "1.0") .put("params", IotDeviceEventPostReqDTO.of( "alarm", MapUtil.builder() diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java new file mode 100644 index 000000000..80d4c9119 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java @@ -0,0 +1,304 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus; + +import com.ghgande.j2mod.modbus.io.ModbusRTUTCPTransport; +import com.ghgande.j2mod.modbus.msg.*; +import com.ghgande.j2mod.modbus.procimg.*; +import com.ghgande.j2mod.modbus.slave.ModbusSlave; +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; + +import java.net.ServerSocket; +import java.net.Socket; + +/** + * Modbus RTU over TCP 完整 Demo + * + * 架构:Master(主站)启动 TCP Server 监听 → Slave(从站)主动 TCP 连接上来 + * 通信协议:RTU 帧格式(带 CRC)通过 TCP 传输,而非标准 MBAP 头 + * + * 流程: + * 1. Master 启动 TCP ServerSocket 监听端口 + * 2. Slave(从站模拟器)作为 TCP Client 连接到 Master + * 3. Master 通过 accept 得到的 Socket,使用 {@link ModbusRTUTCPTransport} 发送读写请求 + * + * 实现说明: + * 因为 j2mod 的 ModbusSlave 只能以 TCP Server 模式运行(监听端口等待 Master 连接), + * 不支持"Slave 作为 TCP Client 主动连接 Master"的模式。 + * 所以这里用一个 TCP 桥接(bridge)来模拟: + * - Slave 在本地内部端口启动(RTU over TCP 模式) + * - 一个桥接线程同时连接 Master Server 和 Slave 内部端口,做双向数据转发 + * - Master 视角:看到的是 Slave 主动连上来 + * + * 依赖:j2mod 3.2.1(pom.xml 中已声明) + * + * @author 芋道源码 + */ +@Deprecated // 仅技术演示,非是必须的 +public class ModbusRtuOverTcpDemo { + + /** + * Master(主站)TCP Server 监听端口 + */ + private static final int PORT = 5021; + /** + * Slave 内部端口(仅本地中转用,不对外暴露) + */ + private static final int SLAVE_INTERNAL_PORT = PORT + 100; + /** + * Modbus 从站地址 + */ + private static final int SLAVE_ID = 1; + + public static void main(String[] args) throws Exception { + // ===================== 第一步:Master 启动 TCP Server 监听 ===================== + ServerSocket serverSocket = new ServerSocket(PORT); + System.out.println("==================================================="); + System.out.println("[Master] TCP Server 已启动,监听端口: " + PORT); + System.out.println("[Master] 等待 Slave 连接..."); + System.out.println("==================================================="); + + // ===================== 第二步:后台启动 Slave,它会主动连接 Master ===================== + ModbusSlave slave = startSlaveInBackground(); + + // Master accept Slave 的连接 + Socket slaveSocket = serverSocket.accept(); + System.out.println("[Master] Slave 已连接: " + slaveSocket.getRemoteSocketAddress()); + + // ===================== 第三步:Master 通过 RTU over TCP 发送读写请求 ===================== + // 使用 ModbusRTUTCPTransport 包装 Socket(RTU 帧 = SlaveID + 功能码 + 数据 + CRC,无 MBAP 头) + ModbusRTUTCPTransport transport = new ModbusRTUTCPTransport(slaveSocket); + + try { + System.out.println("[Master] RTU over TCP 通道已建立\n"); + + // 1. 读操作演示:4 种功能码 + demoReadCoils(transport); // 功能码 01:读线圈 + demoReadDiscreteInputs(transport); // 功能码 02:读离散输入 + demoReadHoldingRegisters(transport); // 功能码 03:读保持寄存器 + demoReadInputRegisters(transport); // 功能码 04:读输入寄存器 + + // 2. 写操作演示 + 读回验证 + demoWriteCoil(transport); // 功能码 05:写单个线圈 + demoWriteRegister(transport); // 功能码 06:写单个保持寄存器 + + System.out.println("\n==================================================="); + System.out.println("所有 RTU over TCP 读写操作执行成功!"); + System.out.println("==================================================="); + } finally { + // 清理资源 + transport.close(); + slaveSocket.close(); + serverSocket.close(); + slave.close(); + System.out.println("[Master] 资源已关闭"); + } + } + + // ===================== Slave 设备模拟(作为 TCP Client 连接 Master) ===================== + + /** + * 在后台启动从站模拟器,并通过 TCP 桥接连到 Master Server + * + * @return ModbusSlave 实例(用于最后关闭资源) + */ + private static ModbusSlave startSlaveInBackground() throws Exception { + // 1. 创建进程映像,初始化寄存器数据 + SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID); + // 1.1 线圈(Coil,功能码 01/05)- 可读写,地址 0~9 + for (int i = 0; i < 10; i++) { + spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0)); + } + // 1.2 离散输入(Discrete Input,功能码 02)- 只读,地址 0~9 + for (int i = 0; i < 10; i++) { + spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0)); + } + // 1.3 保持寄存器(Holding Register,功能码 03/06/16)- 可读写,地址 0~19 + for (int i = 0; i < 20; i++) { + spi.addRegister(new SimpleRegister(i * 100)); + } + // 1.4 输入寄存器(Input Register,功能码 04)- 只读,地址 0~19 + for (int i = 0; i < 20; i++) { + spi.addInputRegister(new SimpleInputRegister(i * 10 + 1)); + } + + // 2. 启动 Slave(RTU over TCP 模式,在本地内部端口监听) + ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(SLAVE_INTERNAL_PORT, 5, true); + slave.addProcessImage(SLAVE_ID, spi); + slave.open(); + System.out.println("[Slave] 从站模拟器已启动(内部端口: " + SLAVE_INTERNAL_PORT + ")"); + + // 3. 启动桥接线程:TCP Client 连接 Master Server,同时连接 Slave 内部端口,双向转发 + Thread bridgeThread = new Thread(() -> { + try { + Socket toMaster = new Socket("127.0.0.1", PORT); + Socket toSlave = new Socket("127.0.0.1", SLAVE_INTERNAL_PORT); + System.out.println("[Bridge] 已建立桥接: Master(" + PORT + ") <-> Slave(" + SLAVE_INTERNAL_PORT + ")"); + + // 双向桥接:Master ↔ Bridge ↔ Slave + Thread forward = new Thread(() -> bridge(toMaster, toSlave), "bridge-master→slave"); + Thread backward = new Thread(() -> bridge(toSlave, toMaster), "bridge-slave→master"); + forward.setDaemon(true); + backward.setDaemon(true); + forward.start(); + backward.start(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "bridge-setup"); + bridgeThread.setDaemon(true); + bridgeThread.start(); + + return slave; + } + + /** + * TCP 双向桥接:从 src 读取数据,写入 dst + */ + private static void bridge(Socket src, Socket dst) { + try { + byte[] buf = new byte[1024]; + var in = src.getInputStream(); + var out = dst.getOutputStream(); + int len; + while ((len = in.read(buf)) != -1) { + out.write(buf, 0, len); + out.flush(); + } + } catch (Exception ignored) { + // 连接关闭时正常退出 + } + } + + // ===================== Master 读写操作 ===================== + + /** + * 发送请求并接收响应(通用方法) + */ + private static ModbusResponse sendRequest(ModbusRTUTCPTransport transport, ModbusRequest request) throws Exception { + request.setUnitID(SLAVE_ID); + transport.writeRequest(request); + return transport.readResponse(); + } + + /** + * 功能码 01:读线圈(Read Coils) + */ + private static void demoReadCoils(ModbusRTUTCPTransport transport) throws Exception { + ReadCoilsRequest request = new ReadCoilsRequest(0, 5); + ReadCoilsResponse response = (ReadCoilsResponse) sendRequest(transport, request); + + StringBuilder sb = new StringBuilder("[功能码 01] 读线圈(0~4): "); + for (int i = 0; i < 5; i++) { + sb.append(response.getCoilStatus(i) ? "ON" : "OFF"); + if (i < 4) { + sb.append(", "); + } + } + System.out.println(sb); + } + + /** + * 功能码 02:读离散输入(Read Discrete Inputs) + */ + private static void demoReadDiscreteInputs(ModbusRTUTCPTransport transport) throws Exception { + ReadInputDiscretesRequest request = new ReadInputDiscretesRequest(0, 5); + ReadInputDiscretesResponse response = (ReadInputDiscretesResponse) sendRequest(transport, request); + + StringBuilder sb = new StringBuilder("[功能码 02] 读离散输入(0~4): "); + for (int i = 0; i < 5; i++) { + sb.append(response.getDiscreteStatus(i) ? "ON" : "OFF"); + if (i < 4) { + sb.append(", "); + } + } + System.out.println(sb); + } + + /** + * 功能码 03:读保持寄存器(Read Holding Registers) + */ + private static void demoReadHoldingRegisters(ModbusRTUTCPTransport transport) throws Exception { + ReadMultipleRegistersRequest request = new ReadMultipleRegistersRequest(0, 5); + ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) sendRequest(transport, request); + + StringBuilder sb = new StringBuilder("[功能码 03] 读保持寄存器(0~4): "); + for (int i = 0; i < response.getWordCount(); i++) { + sb.append(response.getRegisterValue(i)); + if (i < response.getWordCount() - 1) { + sb.append(", "); + } + } + System.out.println(sb); + } + + /** + * 功能码 04:读输入寄存器(Read Input Registers) + */ + private static void demoReadInputRegisters(ModbusRTUTCPTransport transport) throws Exception { + ReadInputRegistersRequest request = new ReadInputRegistersRequest(0, 5); + ReadInputRegistersResponse response = (ReadInputRegistersResponse) sendRequest(transport, request); + + StringBuilder sb = new StringBuilder("[功能码 04] 读输入寄存器(0~4): "); + for (int i = 0; i < response.getWordCount(); i++) { + sb.append(response.getRegisterValue(i)); + if (i < response.getWordCount() - 1) { + sb.append(", "); + } + } + System.out.println(sb); + } + + /** + * 功能码 05:写单个线圈(Write Single Coil)+ 读回验证 + */ + private static void demoWriteCoil(ModbusRTUTCPTransport transport) throws Exception { + int address = 0; + + // 1. 先读取当前值 + ReadCoilsRequest readReq = new ReadCoilsRequest(address, 1); + ReadCoilsResponse readResp = (ReadCoilsResponse) sendRequest(transport, readReq); + boolean beforeValue = readResp.getCoilStatus(0); + + // 2. 写入相反的值 + boolean writeValue = !beforeValue; + WriteCoilRequest writeReq = new WriteCoilRequest(address, writeValue); + sendRequest(transport, writeReq); + + // 3. 读回验证 + ReadCoilsResponse verifyResp = (ReadCoilsResponse) sendRequest(transport, readReq); + boolean afterValue = verifyResp.getCoilStatus(0); + + System.out.println("[功能码 05] 写线圈: 地址=" + address + + ", 写入前=" + (beforeValue ? "ON" : "OFF") + + ", 写入值=" + (writeValue ? "ON" : "OFF") + + ", 读回值=" + (afterValue ? "ON" : "OFF") + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败")); + } + + /** + * 功能码 06:写单个保持寄存器(Write Single Register)+ 读回验证 + */ + private static void demoWriteRegister(ModbusRTUTCPTransport transport) throws Exception { + int address = 0; + int writeValue = 12345; + + // 1. 先读取当前值 + ReadMultipleRegistersRequest readReq = new ReadMultipleRegistersRequest(address, 1); + ReadMultipleRegistersResponse readResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq); + int beforeValue = readResp.getRegisterValue(0); + + // 2. 写入新值 + WriteSingleRegisterRequest writeReq = new WriteSingleRegisterRequest(address, new SimpleRegister(writeValue)); + sendRequest(transport, writeReq); + + // 3. 读回验证 + ReadMultipleRegistersResponse verifyResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq); + int afterValue = verifyResp.getRegisterValue(0); + + System.out.println("[功能码 06] 写保持寄存器: 地址=" + address + + ", 写入前=" + beforeValue + + ", 写入值=" + writeValue + + ", 读回值=" + afterValue + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败")); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java new file mode 100644 index 000000000..62724b2f4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient; + +import com.ghgande.j2mod.modbus.procimg.*; +import com.ghgande.j2mod.modbus.slave.ModbusSlave; +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * Modbus TCP 从站模拟器(手动测试) + * + *

测试场景:模拟一个标准 Modbus TCP 从站设备,供 Modbus TCP Client 网关连接和读写数据 + * + *

使用步骤: + *

    + *
  1. 运行 {@link #testStartSlaveSimulator()} 启动模拟从站(默认端口 5020,从站地址 1)
  2. + *
  3. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-client 协议)
  4. + *
  5. 确保数据库有对应的 Modbus Client 设备配置(ip=127.0.0.1, port=5020, slaveId=1)
  6. + *
  7. 网关会自动连接模拟从站并开始轮询读取寄存器数据
  8. + *
  9. 模拟器每 5 秒自动更新输入寄存器和保持寄存器的值,模拟传感器数据变化
  10. + *
+ * + *

可用寄存器: + *

    + *
  • 线圈 (Coil, 功能码 01/05): 地址 0-9,交替 true/false
  • + *
  • 离散输入 (Discrete Input, 功能码 02): 地址 0-9,每 3 个一个 true
  • + *
  • 保持寄存器 (Holding Register, 功能码 03/06/16): 地址 0-19,初始值 0,100,200,...
  • + *
  • 输入寄存器 (Input Register, 功能码 04): 地址 0-19,初始值 1,11,21,...
  • + *
+ * + * @author 芋道源码 + */ +@Slf4j +@Disabled +public class IoTModbusTcpClientIntegrationTest { + + private static final int PORT = 5020; + private static final int SLAVE_ID = 1; + + /** + * 启动 Modbus TCP 从站模拟器 + * + *

模拟器会持续运行,每 5 秒更新一次寄存器数据,直到手动停止 + */ + @SuppressWarnings({"InfiniteLoopStatement", "BusyWait"}) + @Test + public void testStartSlaveSimulator() throws Exception { + // 1. 创建进程映像(Process Image),存储寄存器数据 + SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID); + + // 2.1 初始化线圈(Coil,功能码 01/05)- 离散输出,可读写 + // 地址 0-9,共 10 个线圈 + for (int i = 0; i < 10; i++) { + spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0)); // 交替 true/false + } + + // 2.2 初始化离散输入(Discrete Input,功能码 02)- 只读 + // 地址 0-9,共 10 个离散输入 + for (int i = 0; i < 10; i++) { + spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0)); // 每 3 个一个 true + } + + // 2.3 初始化保持寄存器(Holding Register,功能码 03/06/16)- 可读写 + // 地址 0-19,共 20 个寄存器 + for (int i = 0; i < 20; i++) { + spi.addRegister(new SimpleRegister(i * 100)); // 值为 0, 100, 200, ... + } + + // 2.4 初始化输入寄存器(Input Register,功能码 04)- 只读 + // 地址 0-19,共 20 个寄存器 + SimpleInputRegister[] inputRegisters = new SimpleInputRegister[20]; + for (int i = 0; i < 20; i++) { + inputRegisters[i] = new SimpleInputRegister(i * 10 + 1); // 值为 1, 11, 21, ... + spi.addInputRegister(inputRegisters[i]); + } + + // 3.1 创建 Modbus TCP 从站 + ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(PORT, 5); + slave.addProcessImage(SLAVE_ID, spi); + // 3.2 启动从站 + slave.open(); + + log.info("[testStartSlaveSimulator][Modbus TCP 从站模拟器已启动, 端口: {}, 从站地址: {}]", PORT, SLAVE_ID); + log.info("[testStartSlaveSimulator][可用寄存器: 线圈(01/05) 0-9, 离散输入(02) 0-9, " + + "保持寄存器(03/06/16) 0-19, 输入寄存器(04) 0-19]"); + + // 4. 添加关闭钩子 + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + log.info("[testStartSlaveSimulator][正在关闭模拟器...]"); + slave.close(); + log.info("[testStartSlaveSimulator][模拟器已关闭]"); + })); + + // 5. 保持运行,定时更新输入寄存器模拟数据变化 + int counter = 0; + while (true) { + Thread.sleep(5000); // 每 5 秒更新一次 + counter++; + + // 更新输入寄存器的值,模拟传感器数据变化 + for (int i = 0; i < 20; i++) { + int newValue = (i * 10 + 1) + counter; + inputRegisters[i].setValue(newValue); + } + + // 更新保持寄存器的第一个值 + spi.getRegister(0).setValue(counter * 100); + log.info("[testStartSlaveSimulator][数据已更新, counter={}, 保持寄存器[0]={}, 输入寄存器[0]={}]", + counter, counter * 100, 1 + counter); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java new file mode 100644 index 000000000..24029a19e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java @@ -0,0 +1,302 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import io.vertx.core.Vertx; +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 org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * IoT Modbus TCP Server 协议集成测试 — MODBUS_RTU 帧格式(手动测试) + * + *

测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_RTU(CRC16)帧格式通信 + * + *

使用步骤: + *

    + *
  1. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)
  2. + *
  3. 确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_rtu)
  4. + *
  5. 运行以下测试方法: + *
      + *
    • {@link #testAuth()} - 自定义功能码认证
    • + *
    • {@link #testPollingResponse()} - 轮询响应
    • + *
    • {@link #testPropertySetWrite()} - 属性设置(接收写指令)
    • + *
    + *
  6. + *
+ * + * @author 芋道源码 + */ +@Slf4j +@Disabled +public class IotModbusTcpServerRtuIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 503; + private static final int TIMEOUT_MS = 5000; + + private static final int CUSTOM_FC = 65; + private static final int SLAVE_ID = 1; + + private static Vertx vertx; + private static NetClient netClient; + + // ===================== 编解码器 ===================== + + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC); + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC); + + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== + + private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo"; + private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_rtu"; + private static final String DEVICE_SECRET = "af01c55eb8e3424bb23fc6c783936b2e"; + + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + + // ===================== 认证测试 ===================== + + /** + * 认证测试:发送自定义功能码 FC65 认证帧(RTU 格式),验证认证成功响应 + */ + @Test + public void testAuth() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 构造并发送认证帧 + IotModbusFrame response = authenticate(socket); + + // 2. 验证响应 + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", + response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); + JSONObject json = JSONUtil.parseObj(response.getCustomData()); + assertEquals(0, json.getInt("code")); + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); + } finally { + socket.close(); + } + } + + // ===================== 轮询响应测试 ===================== + + /** + * 轮询响应测试:认证后持续监听网关下发的读请求(RTU 格式),每次收到都自动构造读响应帧发回 + */ + @Test + public void testPollingResponse() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 先认证 + IotModbusFrame authResponse = authenticate(socket); + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData()); + assertEquals(0, authJson.getInt("code")); + + // 2. 设置持续监听:每收到一个读请求,自动回复 + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); + CompletableFuture done = new CompletableFuture<>(); + // 注意:使用 requestMode=true,因为设备端收到的是网关下发的读请求(非响应) + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]", + frame.getSlaveId(), frame.getFunctionCode()); + // 解析读请求中的起始地址和数量 + byte[] pdu = frame.getPdu(); + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); + + // 构造读响应帧(模拟寄存器数据,RTU 格式) + int[] registerValues = new int[quantity]; + for (int i = 0; i < quantity; i++) { + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... + } + byte[] responseData = buildReadResponse(frame.getSlaveId(), + frame.getFunctionCode(), registerValues); + socket.write(Buffer.buffer(responseData)); + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); + }, true); + socket.handler(parser); + + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 + Thread.sleep(200000); + } finally { + socket.close(); + } + } + + // ===================== 属性设置测试 ===================== + + /** + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求(RTU 格式) + *

+ * 注意:需手动在平台触发 property.set + */ + @Test + public void testPropertySetWrite() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 先认证 + IotModbusFrame authResponse = authenticate(socket); + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData()); + + // 2. 等待网关下发写请求(需手动在平台触发 property.set) + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]"); + IotModbusFrame writeRequest = waitForRequest(socket); + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]", + writeRequest.getSlaveId(), writeRequest.getFunctionCode(), + HexUtil.encodeHexStr(writeRequest.getPdu())); + } finally { + socket.close(); + } + } + + // ===================== 辅助方法 ===================== + + /** + * 建立 TCP 连接 + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + + /** + * 执行认证并返回响应帧 + */ + private IotModbusFrame authenticate(NetSocket socket) throws Exception { + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + authInfo.setClientId(""); + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); + return sendAndReceive(socket, authFrame); + } + + /** + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码) + */ + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { + CompletableFuture responseFuture = new CompletableFuture<>(); + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame) + RecordParser parser = FRAME_DECODER.createRecordParser( + (frame, frameFormat) -> { + try { + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat); + responseFuture.complete(frame); + } catch (Exception e) { + responseFuture.completeExceptionally(e); + } + }); + socket.handler(parser); + + // 发送请求 + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length); + socket.write(Buffer.buffer(frameData)); + + // 等待响应 + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + /** + * 等待接收网关下发的请求帧(不发送,只等待接收) + */ + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { + CompletableFuture requestFuture = new CompletableFuture<>(); + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame) + RecordParser parser = FRAME_DECODER.createRecordParser( + (frame, frameFormat) -> { + try { + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat); + requestFuture.complete(frame); + } catch (Exception e) { + requestFuture.completeExceptionally(e); + } + }); + socket.handler(parser); + + // 等待(超时 30 秒,因为轮询间隔可能比较长) + return requestFuture.get(30000, TimeUnit.MILLISECONDS); + } + + /** + * 构造认证帧(MODBUS_RTU 格式) + *

+ * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}} + *

+ * RTU 帧格式:[SlaveId(1)] [FC=0x41(1)] [ByteCount(1)] [JSON(N)] [CRC16(2)] + */ + private byte[] buildAuthFrame(String clientId, String username, String password) { + JSONObject params = new JSONObject(); + params.set("clientId", clientId); + params.set("username", username); + params.set("password", password); + JSONObject json = new JSONObject(); + json.set("method", "auth"); + json.set("params", params); + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), + IotModbusFrameFormatEnum.MODBUS_RTU, 0); + } + + /** + * 构造 FC03/FC01-04 读响应帧(MODBUS_RTU 格式) + *

+ * RTU 帧格式:[SlaveId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] [CRC16(2)] + */ + private byte[] buildReadResponse(int slaveId, int functionCode, int[] registerValues) { + int byteCount = registerValues.length * 2; + // 帧长度:SlaveId(1) + FC(1) + ByteCount(1) + Data(N*2) + CRC(2) + int totalLength = 1 + 1 + 1 + byteCount + 2; + byte[] frame = new byte[totalLength]; + frame[0] = (byte) slaveId; + frame[1] = (byte) functionCode; + frame[2] = (byte) byteCount; + for (int i = 0; i < registerValues.length; i++) { + frame[3 + i * 2] = (byte) ((registerValues[i] >> 8) & 0xFF); + frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF); + } + // 计算 CRC16 + int crc = IotModbusCommonUtils.calculateCrc16(frame, totalLength - 2); + frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low + frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High + return frame; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java new file mode 100644 index 000000000..d00da5fe8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java @@ -0,0 +1,302 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder; +import io.vertx.core.Vertx; +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 org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * IoT Modbus TCP Server 协议集成测试 — MODBUS_TCP 帧格式(手动测试) + * + *

测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_TCP(MBAP 头)帧格式通信 + * + *

使用步骤: + *

    + *
  1. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)
  2. + *
  3. 确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_tcp)
  4. + *
  5. 运行以下测试方法: + *
      + *
    • {@link #testAuth()} - 自定义功能码认证
    • + *
    • {@link #testPollingResponse()} - 轮询响应
    • + *
    • {@link #testPropertySetWrite()} - 属性设置(接收写指令)
    • + *
    + *
  6. + *
+ * + * @author 芋道源码 + */ +@Slf4j +@Disabled +public class IotModbusTcpServerTcpIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 503; + private static final int TIMEOUT_MS = 5000; + + private static final int CUSTOM_FC = 65; + private static final int SLAVE_ID = 1; + + private static Vertx vertx; + private static NetClient netClient; + + // ===================== 编解码器 ===================== + + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC); + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC); + + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== + + private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo"; + private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_tcp"; + private static final String DEVICE_SECRET = "8e4adeb3d25342ab88643421d3fba3f6"; + + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + + // ===================== 认证测试 ===================== + + /** + * 认证测试:发送自定义功能码 FC65 认证帧,验证认证成功响应 + */ + @Test + public void testAuth() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 构造并发送认证帧 + IotModbusFrame response = authenticate(socket); + + // 2. 验证响应 + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]", + response.getSlaveId(), response.getFunctionCode(), response.getCustomData()); + JSONObject json = JSONUtil.parseObj(response.getCustomData()); + assertEquals(0, json.getInt("code")); + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message")); + } finally { + socket.close(); + } + } + + // ===================== 轮询响应测试 ===================== + + /** + * 轮询响应测试:认证后持续监听网关下发的读请求,每次收到都自动构造读响应帧发回 + */ + @Test + public void testPollingResponse() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 先认证 + IotModbusFrame authResponse = authenticate(socket); + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData()); + assertEquals(0, authJson.getInt("code")); + + // 2. 设置持续监听:每收到一个读请求,自动回复 + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}, transactionId={}]", + frame.getSlaveId(), frame.getFunctionCode(), frame.getTransactionId()); + // 解析读请求中的起始地址和数量 + byte[] pdu = frame.getPdu(); + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); + + // 构造读响应帧(模拟寄存器数据) + int[] registerValues = new int[quantity]; + for (int i = 0; i < quantity; i++) { + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... + } + byte[] responseData = buildReadResponse(frame.getTransactionId(), + frame.getSlaveId(), frame.getFunctionCode(), registerValues); + socket.write(Buffer.buffer(responseData)); + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); + }); + socket.handler(parser); + + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 + Thread.sleep(200000); + } finally { + socket.close(); + } + } + + // ===================== 属性设置测试 ===================== + + /** + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求 + *

+ * 注意:需手动在平台触发 property.set + */ + @Test + public void testPropertySetWrite() throws Exception { + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + // 1. 先认证 + IotModbusFrame authResponse = authenticate(socket); + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData()); + + // 2. 等待网关下发写请求(需手动在平台触发 property.set) + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]"); + IotModbusFrame writeRequest = waitForRequest(socket); + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, transactionId={}, pdu={}]", + writeRequest.getSlaveId(), writeRequest.getFunctionCode(), + writeRequest.getTransactionId(), HexUtil.encodeHexStr(writeRequest.getPdu())); + } finally { + socket.close(); + } + } + + // ===================== 辅助方法 ===================== + + /** + * 建立 TCP 连接 + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + + /** + * 执行认证并返回响应帧 + */ + private IotModbusFrame authenticate(NetSocket socket) throws Exception { + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); + authInfo.setClientId(""); // 特殊:考虑到 modbus 消息长度限制,默认 clientId 不发送 + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); + return sendAndReceive(socket, authFrame); + } + + /** + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码) + */ + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { + CompletableFuture responseFuture = new CompletableFuture<>(); + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame) + RecordParser parser = FRAME_DECODER.createRecordParser( + (frame, frameFormat) -> { + try { + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat); + responseFuture.complete(frame); + } catch (Exception e) { + responseFuture.completeExceptionally(e); + } + }); + socket.handler(parser); + + // 发送请求 + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length); + socket.write(Buffer.buffer(frameData)); + + // 等待响应 + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + /** + * 等待接收网关下发的请求帧(不发送,只等待接收) + */ + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { + CompletableFuture requestFuture = new CompletableFuture<>(); + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame) + RecordParser parser = FRAME_DECODER.createRecordParser( + (frame, frameFormat) -> { + try { + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat); + requestFuture.complete(frame); + } catch (Exception e) { + requestFuture.completeExceptionally(e); + } + }); + socket.handler(parser); + + // 等待(超时 30 秒,因为轮询间隔可能比较长) + return requestFuture.get(30000, TimeUnit.MILLISECONDS); + } + + /** + * 构造认证帧(MODBUS_TCP 格式) + *

+ * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}} + */ + private byte[] buildAuthFrame(String clientId, String username, String password) { + JSONObject params = new JSONObject(); + params.set("clientId", clientId); + params.set("username", username); + params.set("password", password); + JSONObject json = new JSONObject(); + json.set("method", "auth"); + json.set("params", params); + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), + IotModbusFrameFormatEnum.MODBUS_TCP, 1); + } + + /** + * 构造 FC03/FC01-04 读响应帧(MODBUS_TCP 格式) + *

+ * 格式:[MBAP(6)] [UnitId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] + */ + private byte[] buildReadResponse(int transactionId, int slaveId, int functionCode, int[] registerValues) { + int byteCount = registerValues.length * 2; + // PDU: FC(1) + ByteCount(1) + Data(N*2) + int pduLength = 1 + 1 + byteCount; + // 完整帧:MBAP(6) + UnitId(1) + PDU + int totalLength = 6 + 1 + pduLength; + ByteBuffer buf = ByteBuffer.allocate(totalLength).order(ByteOrder.BIG_ENDIAN); + // MBAP Header + buf.putShort((short) transactionId); // Transaction ID + buf.putShort((short) 0); // Protocol ID + buf.putShort((short) (1 + pduLength)); // Length (UnitId + PDU) + // UnitId + buf.put((byte) slaveId); + // PDU + buf.put((byte) functionCode); + buf.put((byte) byteCount); + for (int value : registerValues) { + buf.putShort((short) value); + } + return buf.array(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java index 67a8ced4d..3333af6a7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java @@ -1,16 +1,15 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -23,7 +22,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -59,9 +57,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -88,39 +86,19 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); + MqttClient client = createClient(authInfo); + try { + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + } finally { + disconnect(client); } } @@ -135,27 +113,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testPropertyPost][连接认证成功]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), + IotDevicePropertyPostReqDTO.of(MapUtil.builder() + .put("width", 1) + .put("height", "2") + .build())); - // 3. 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("width", 1) - .put("height", "2") - .build()), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + // 2.2 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 直连设备事件上报测试 ===================== @@ -169,27 +146,26 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testEventPost][连接认证成功]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), + IotDeviceEventPostReqDTO.of( + "eat", + MapUtil.builder().put("rice", 3).build(), + System.currentTimeMillis())); - // 3. 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "eat", - MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testEventPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 设备动态注册测试(一型一密) ===================== @@ -197,37 +173,58 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { /** * 直连设备动态注册测试(一型一密) *

- * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret) + * 认证方式: + * - clientId: 任意值 + "|authType=register|" 后缀 + * - username: {deviceName}&{productKey}(与普通认证相同) + * - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256) *

- * 注意:此接口不需要认证 + * 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证 */ @Test public void testDeviceRegister() throws Exception { - // 1. 连接并认证(使用已有设备连接) - MqttClient client = connectAndAuth(); - log.info("[testDeviceRegister][连接认证成功]"); + // 1.1 构建注册参数 + String deviceName = "test-mqtt-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + // 1.2 构建 MQTT 连接参数(clientId 需要添加 |authType=register| 后缀) + String clientId = IotDeviceAuthUtils.buildClientId(PRODUCT_KEY, deviceName) + "|authType=register|"; + String username = IotDeviceAuthUtils.buildUsername(PRODUCT_KEY, deviceName); + log.info("[testDeviceRegister][注册参数: clientId={}, username={}, sign={}]", + clientId, username, sign); + // 1.3 创建客户端并连接(连接时服务端自动处理注册) + MqttClientOptions options = new MqttClientOptions() + .setClientId(clientId) + .setUsername(username) + .setPassword(sign) + .setCleanSession(true) + .setKeepAliveInterval(60); + MqttClient client = MqttClient.create(vertx, options); - // 2.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-mqtt-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 2.2 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - subscribeReply(client, replyTopic); + try { + // 2. 连接服务器(连接成功后服务端会自动处理注册并发送响应) + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testDeviceRegister][连接成功,等待注册响应...]"); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/auth/register", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + // 3.1 设置消息处理器,接收注册响应 + CompletableFuture responseFuture = new CompletableFuture<>(); + client.publishHandler(message -> { + log.info("[testDeviceRegister][收到响应: topic={}, payload={}]", + message.topicName(), message.payload().toString()); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); + responseFuture.complete(response); + }); + // 3.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", PRODUCT_KEY, deviceName); + subscribe(client, replyTopic); - // 4. 断开连接 - disconnect(client); + // 4. 等待注册响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testDeviceRegister][注册响应: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + } finally { + disconnect(client); + } } // ===================== 订阅下行消息测试 ===================== @@ -237,44 +234,41 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { */ @Test public void testSubscribe() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 连接并认证 MqttClient client = connectAndAuth(); log.info("[testSubscribe][连接认证成功]"); - // 2. 设置消息处理器 - client.publishHandler(message -> { - log.info("[testSubscribe][收到消息: topic={}, payload={}]", - message.topicName(), message.payload().toString()); - }); - - // 3. 订阅下行主题 - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); - log.info("[testSubscribe][订阅主题: {}]", topic); - - client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(subscribeAr -> { - if (subscribeAr.succeeded()) { - log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); - // 保持连接 30 秒等待消息 - vertx.setTimer(30000, id -> { - client.disconnect() - .onComplete(disconnectAr -> { - log.info("[testSubscribe][断开连接]"); - latch.countDown(); - }); - }); - } else { - log.error("[testSubscribe][订阅失败]", subscribeAr.cause()); - latch.countDown(); + try { + // 2. 设置消息处理器:收到属性设置时,回复 _reply 消息 + client.publishHandler(message -> { + log.info("[testSubscribe][收到消息: topic={}, payload={}]", + message.topicName(), message.payload().toString()); + // 收到属性设置消息时,回复 _reply + if (message.topicName().endsWith("/thing/property/set")) { + try { + IotDeviceMessage received = SERIALIZER.deserialize(message.payload().getBytes()); + IotDeviceMessage reply = IotDeviceMessage.replyOf( + received.getRequestId(), "thing.property.set_reply", null, 0, null); + String replyTopic = String.format("/sys/%s/%s/thing/property/set_reply", PRODUCT_KEY, DEVICE_NAME); + byte[] replyPayload = SERIALIZER.serialize(reply); + client.publish(replyTopic, Buffer.buffer(replyPayload), MqttQoS.AT_LEAST_ONCE, false, false); + log.info("[testSubscribe][已回复属性设置: topic={}]", replyTopic); + } catch (Exception e) { + log.error("[testSubscribe][回复属性设置异常]", e); } - }); + } + }); - // 4. 等待测试完成 - boolean completed = latch.await(60, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testSubscribe][测试超时]"); + // 3. 订阅下行主题(属性设置 + 服务调用) + String topic = String.format("/sys/%s/%s/#", PRODUCT_KEY, DEVICE_NAME); + log.info("[testSubscribe][订阅主题: {}]", topic); + subscribe(client, topic); + log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); + + // 4. 保持连接 30 秒等待消息 + Thread.sleep(30000); + } finally { + disconnect(client); } } @@ -286,7 +280,7 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @param authInfo 认证信息 * @return MQTT 客户端 */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { MqttClientOptions options = new MqttClientOptions() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) @@ -302,44 +296,23 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @return 已认证的 MQTT 客户端 */ private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); + MqttClient client = createClient(authInfo); client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; } /** - * 订阅响应主题 + * 订阅主题 * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 + * @param client MQTT 客户端 + * @param topic 主题 */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); } /** @@ -350,34 +323,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @param request 请求消息 * @return 响应消息 */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) + throws Exception { // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); + responseFuture.complete(response); }); - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); - + // 2. 序列化并发布消息 + byte[] payload = SERIALIZER.serialize(request); + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", + SERIALIZER.getType(), topic, new String(payload)); client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publishAndWaitReply][消息发布成功]"); - // 3. 等待响应(超时返回 null) + // 3. 等待响应 try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[publishAndWaitReply][等待响应超时或失败]"); return null; @@ -390,19 +357,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { * @param client MQTT 客户端 */ private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java index 517206734..8e099749c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewayDeviceMqttProtocolIntegrationTest.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -13,8 +12,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -27,10 +26,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -68,9 +65,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -103,8 +100,6 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); @@ -112,31 +107,13 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); + MqttClient client = createClient(authInfo); + try { + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + } finally { + disconnect(client); } } @@ -153,36 +130,35 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testTopoAdd][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建子设备认证信息 + IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( + SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); + IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() + .setClientId(subAuthInfo.getClientId()) + .setUsername(subAuthInfo.getUsername()) + .setPassword(subAuthInfo.getPassword()); - // 2.2 构建子设备认证信息 - IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( - SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); - IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() - .setClientId(subAuthInfo.getClientId()) - .setUsername(subAuthInfo.getUsername()) - .setPassword(subAuthInfo.getPassword()); + // 2.2 构建请求消息 + IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO() + .setSubDevices(Collections.singletonList(subDeviceAuth)); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), + params); - // 2.3 构建请求消息 - IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); - params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - params, - null, null, null); + // 2.3 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/add_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/add", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoAdd][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/add", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoAdd][响应消息: {}]", response); + } finally { + disconnect(client); + } } /** @@ -196,29 +172,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testTopoDelete][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建请求消息 + IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO() + .setSubDevices(Collections.singletonList( + new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), + params); - // 2.2 构建请求消息 - IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); - params.setSubDevices(Collections.singletonList( - new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - params, - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/delete_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/delete", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoDelete][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/delete", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoDelete][响应消息: {}]", response); + } finally { + disconnect(client); + } } /** @@ -232,27 +207,26 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testTopoGet][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建请求消息 + IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), + params); - // 2.2 构建请求消息 - IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - params, - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/topo/get_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/topo/get", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testTopoGet][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/topo/get", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testTopoGet][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 子设备注册测试 ===================== @@ -270,29 +244,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testSubDeviceRegister][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建请求消息 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO() + .setProductKey(SUB_DEVICE_PRODUCT_KEY) + .setDeviceName("mougezishebei-mqtt"); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), + Collections.singletonList(subDevice)); - // 2.2 构建请求消息 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei-mqtt"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - Collections.singletonList(subDevice), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/auth/sub-device/register_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/auth/sub-device/register", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 批量上报测试 ===================== @@ -308,64 +281,63 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { MqttClient client = connectAndAuth(); log.info("[testPropertyPackPost][连接认证成功]"); - // 2.1 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建【网关设备】自身属性 + Map gatewayProperties = MapUtil.builder() + .put("temperature", 25.5) + .build(); - // 2.2 构建【网关设备】自身属性 - Map gatewayProperties = MapUtil.builder() - .put("temperature", 25.5) - .build(); + // 2.2 构建【网关设备】自身事件 + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("message", "gateway started").build()) + .setTime(System.currentTimeMillis()); + Map gatewayEvents = MapUtil + .builder() + .put("statusReport", gatewayEvent) + .build(); - // 2.3 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); - Map gatewayEvents = MapUtil - .builder() - .put("statusReport", gatewayEvent) - .build(); + // 2.3 构建【网关子设备】属性 + Map subDeviceProperties = MapUtil.builder() + .put("power", 100) + .build(); - // 2.4 构建【网关子设备】属性 - Map subDeviceProperties = MapUtil.builder() - .put("power", 100) - .build(); + // 2.4 构建【网关子设备】事件 + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("errorCode", 0).build()) + .setTime(System.currentTimeMillis()); + Map subDeviceEvents = MapUtil + .builder() + .put("healthCheck", subDeviceEvent) + .build(); - // 2.5 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); - Map subDeviceEvents = MapUtil - .builder() - .put("healthCheck", subDeviceEvent) - .build(); + // 2.5 构建子设备数据 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) + .setProperties(subDeviceProperties) + .setEvents(subDeviceEvents); - // 2.6 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); + // 2.6 构建请求消息 + IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO() + .setProperties(gatewayProperties) + .setEvents(gatewayEvents) + .setSubDevices(ListUtil.of(subDeviceData)); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), + params); - // 2.7 构建请求消息 - IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); - params.setProperties(gatewayProperties); - params.setEvents(gatewayEvents); - params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - params, - null, null, null); + // 2.7 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/event/property/pack/post_reply", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + subscribe(client, replyTopic); - // 3. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", - GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPackPost][响应消息: {}]", response); - - // 4. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/event/property/pack/post", + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testPropertyPackPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 辅助方法 ===================== @@ -376,7 +348,7 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @param authInfo 认证信息 * @return MQTT 客户端 */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { MqttClientOptions options = new MqttClientOptions() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) @@ -392,45 +364,24 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @return 已认证的 MQTT 客户端 */ private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); + MqttClient client = createClient(authInfo); client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; } /** - * 订阅响应主题 + * 订阅主题 * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 + * @param client MQTT 客户端 + * @param topic 主题 */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); } /** @@ -441,34 +392,28 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @param request 请求消息 * @return 响应消息 */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) + throws Exception { // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); + responseFuture.complete(response); }); - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); - + // 2. 序列化并发布消息 + byte[] payload = SERIALIZER.serialize(request); + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", + SERIALIZER.getType(), topic, new String(payload)); client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publishAndWaitReply][消息发布成功]"); - // 3. 等待响应(超时返回 null) + // 3. 等待响应 try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[publishAndWaitReply][等待响应超时或失败]"); return null; @@ -481,19 +426,9 @@ public class IotGatewayDeviceMqttProtocolIntegrationTest { * @param client MQTT 客户端 */ private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java index c14d2c676..ca01b8503 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotGatewaySubDeviceMqttProtocolIntegrationTest.java @@ -1,15 +1,14 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -22,7 +21,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -61,9 +59,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器(MQTT 使用 Alink 协议) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -90,39 +88,19 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - CountDownLatch latch = new CountDownLatch(1); - // 1. 构建认证信息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); log.info("[testAuth][认证信息: clientId={}, username={}, password={}]", authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword()); // 2. 创建客户端并连接 - MqttClient client = connect(authInfo); - client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); - // 断开连接 - client.disconnect() - .onComplete(disconnectAr -> { - if (disconnectAr.succeeded()) { - log.info("[testAuth][断开连接成功]"); - } else { - log.error("[testAuth][断开连接失败]", disconnectAr.cause()); - } - latch.countDown(); - }); - } else { - log.error("[testAuth][连接失败]", ar.cause()); - latch.countDown(); - } - }); - - // 3. 等待测试完成 - boolean completed = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!completed) { - log.warn("[testAuth][测试超时]"); + MqttClient client = createClient(authInfo); + try { + client.connect(SERVER_PORT, SERVER_HOST) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[testAuth][连接成功,客户端 ID: {}]", client.clientId()); + } finally { + disconnect(client); } } @@ -138,28 +116,27 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { log.info("[testPropertyPost][连接认证成功]"); log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), + IotDevicePropertyPostReqDTO.of(MapUtil.builder() + .put("power", 100) + .put("status", "online") + .put("temperature", 36.5) + .build())); - // 3. 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), - IotDevicePropertyPostReqDTO.of(MapUtil.builder() - .put("power", 100) - .put("status", "online") - .put("temperature", 36.5) - .build()), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testPropertyPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/property/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 子设备事件上报测试 ===================== @@ -174,32 +151,31 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { log.info("[testEventPost][连接认证成功]"); log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - // 2. 订阅 _reply 主题 - String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); - subscribeReply(client, replyTopic); + try { + // 2.1 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), + IotDeviceEventPostReqDTO.of( + "alarm", + MapUtil.builder() + .put("level", "warning") + .put("message", "temperature too high") + .put("threshold", 40) + .put("current", 42) + .build(), + System.currentTimeMillis())); - // 3. 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), - IotDeviceEventPostReqDTO.of( - "alarm", - MapUtil.builder() - .put("level", "warning") - .put("message", "temperature too high") - .put("threshold", 40) - .put("current", 42) - .build(), - System.currentTimeMillis()), - null, null, null); + // 2.2 订阅 _reply 主题 + String replyTopic = String.format("/sys/%s/%s/thing/event/post_reply", PRODUCT_KEY, DEVICE_NAME); + subscribe(client, replyTopic); - // 4. 发布消息并等待响应 - String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); - IotDeviceMessage response = publishAndWaitReply(client, topic, request); - log.info("[testEventPost][响应消息: {}]", response); - - // 5. 断开连接 - disconnect(client); + // 3. 发布消息并等待响应 + String topic = String.format("/sys/%s/%s/thing/event/post", PRODUCT_KEY, DEVICE_NAME); + IotDeviceMessage response = publishAndWaitReply(client, topic, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + disconnect(client); + } } // ===================== 辅助方法 ===================== @@ -210,7 +186,7 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @param authInfo 认证信息 * @return MQTT 客户端 */ - private MqttClient connect(IotDeviceAuthReqDTO authInfo) { + private MqttClient createClient(IotDeviceAuthReqDTO authInfo) { MqttClientOptions options = new MqttClientOptions() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) @@ -226,44 +202,23 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @return 已认证的 MQTT 客户端 */ private MqttClient connectAndAuth() throws Exception { - // 1. 创建客户端并连接 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - MqttClient client = connect(authInfo); - - // 2.1 连接 - CompletableFuture future = new CompletableFuture<>(); + MqttClient client = createClient(authInfo); client.connect(SERVER_PORT, SERVER_HOST) - .onComplete(ar -> { - if (ar.succeeded()) { - future.complete(client); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2.2 等待连接结果 - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return client; } /** - * 订阅响应主题 + * 订阅主题 * - * @param client MQTT 客户端 - * @param replyTopic 响应主题 + * @param client MQTT 客户端 + * @param topic 主题 */ - private void subscribeReply(MqttClient client, String replyTopic) throws Exception { - // 1. 订阅响应主题 - CompletableFuture future = new CompletableFuture<>(); - client.subscribe(replyTopic, MqttQoS.AT_LEAST_ONCE.value()) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[subscribeReply][订阅响应主题成功: {}]", replyTopic); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待订阅结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + private void subscribe(MqttClient client, String topic) throws Exception { + client.subscribe(topic, MqttQoS.AT_LEAST_ONCE.value()) + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[subscribe][订阅主题成功: {}]", topic); } /** @@ -274,34 +229,28 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @param request 请求消息 * @return 响应消息 */ - private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) { + private IotDeviceMessage publishAndWaitReply(MqttClient client, String topic, IotDeviceMessage request) + throws Exception { // 1. 设置消息处理器,接收响应 - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture responseFuture = new CompletableFuture<>(); client.publishHandler(message -> { log.info("[publishAndWaitReply][收到响应: topic={}, payload={}]", message.topicName(), message.payload().toString()); - IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); - future.complete(response); + IotDeviceMessage response = SERIALIZER.deserialize(message.payload().getBytes()); + responseFuture.complete(response); }); - // 2. 编码并发布消息 - byte[] payload = CODEC.encode(request); - log.info("[publishAndWaitReply][Codec: {}, 发送消息: topic={}, payload={}]", - CODEC.type(), topic, new String(payload)); - + // 2. 序列化并发布消息 + byte[] payload = SERIALIZER.serialize(request); + log.info("[publishAndWaitReply][Serializer: {}, 发送消息: topic={}, payload={}]", + SERIALIZER.getType(), topic, new String(payload)); client.publish(topic, Buffer.buffer(payload), MqttQoS.AT_LEAST_ONCE, false, false) - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[publishAndWaitReply][消息发布成功,messageId={}]", ar.result()); - } else { - log.error("[publishAndWaitReply][消息发布失败]", ar.cause()); - future.completeExceptionally(ar.cause()); - } - }); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[publishAndWaitReply][消息发布成功]"); - // 3. 等待响应(超时返回 null) + // 3. 等待响应 try { - return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { log.warn("[publishAndWaitReply][等待响应超时或失败]"); return null; @@ -314,19 +263,9 @@ public class IotGatewaySubDeviceMqttProtocolIntegrationTest { * @param client MQTT 客户端 */ private void disconnect(MqttClient client) throws Exception { - // 1. 断开连接 - CompletableFuture future = new CompletableFuture<>(); client.disconnect() - .onComplete(ar -> { - if (ar.succeeded()) { - log.info("[disconnect][断开连接成功]"); - future.complete(null); - } else { - future.completeExceptionally(ar.cause()); - } - }); - // 2. 等待断开结果 - future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + log.info("[disconnect][断开连接成功]"); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java index 4b6936c63..778c72fd6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java @@ -1,8 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.HexUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -10,32 +8,35 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import io.vertx.core.Vertx; +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 org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 直连设备 TCP 协议集成测试(手动测试) * *

测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 TCP 协议直接连接平台 * - *

支持两种编解码格式: - *

    - *
  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
  • - *
  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
  • - *
- * *

使用步骤: *

    *
  1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
  2. - *
  3. 修改 {@link #CODEC} 选择测试的编解码格式
  4. *
  5. 运行以下测试方法: *
      *
    • {@link #testAuth()} - 设备认证
    • @@ -58,10 +59,31 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + private static Vertx vertx; + private static NetClient netClient; -// private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); - private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + // ===================== 编解码器 ===================== + + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); + + /** + * TCP 帧编解码器 + */ + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create( + new IotTcpConfig.CodecConfig() + .setType(IotTcpCodecTypeEnum.DELIMITER.getType()) + .setDelimiter("\\n") +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setLengthFieldOffset(0) +// .setLengthFieldLength(4) +// .setLengthAdjustment(0) +// .setInitialBytesToStrip(4) +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setFixedLength(256) + ); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -69,6 +91,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { private static final String DEVICE_NAME = "small"; private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3"; + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + // ===================== 认证测试 ===================== /** @@ -76,28 +117,21 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testAuth][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -112,29 +146,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testDeviceRegister() throws Exception { - // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-tcp-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + // 1. 构建注册消息 + String deviceName = "test-tcp-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName(deviceName) + .setSign(sign); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } else { - log.warn("[testDeviceRegister][未收到响应]"); - } + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testDeviceRegister][响应消息: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + } finally { + socket.close(); } } @@ -145,35 +175,25 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testPropertyPost][认证响应: {}]", authResponse); - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("width", 1) .put("height", "2") - .build()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + .build())); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -184,95 +204,87 @@ public class IotDirectDeviceTcpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testEventPost][认证响应: {}]", authResponse); - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), IotDeviceEventPostReqDTO.of( "eat", MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + System.currentTimeMillis())); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + socket.close(); } } // ===================== 辅助方法 ===================== + /** + * 建立 TCP 连接 + * + * @return 连接 Future + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + /** * 执行设备认证 * * @param socket TCP 连接 * @return 认证响应消息 */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { + private IotDeviceMessage authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - log.info("[authenticate][响应数据长度: {} 字节,首字节: 0x{}, HEX: {}]", - responseBytes.length, - String.format("%02X", responseBytes[0]), - HexUtil.encodeHexStr(responseBytes)); - return CODEC.decode(responseBytes); - } - return null; + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo); + return sendAndReceive(socket, request); } /** - * 发送 TCP 请求并接收响应 + * 发送消息并接收响应 * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param socket TCP 连接 + * @param request 请求消息 + * @return 响应消息 */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; + private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception { + // 1. 使用 FRAME_CODEC 创建解码器 + CompletableFuture responseFuture = new CompletableFuture<>(); + RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> { + try { + // 反序列化响应 + IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes()); + responseFuture.complete(response); + } catch (Exception e) { + responseFuture.completeExceptionally(e); } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } + }); + socket.handler(parser); + + // 2.1 序列化 + 帧编码 + byte[] serializedData = SERIALIZER.serialize(request); + Buffer frameData = FRAME_CODEC.encode(serializedData); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); + // 2.2 发送请求 + socket.write(frameData); + + // 3. 等待响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", SERIALIZER.serialize(response).length); + return response; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java index b417ceb9f..5bb113b91 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewayDeviceTcpProtocolIntegrationTest.java @@ -13,35 +13,36 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import io.vertx.core.Vertx; +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 org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; import java.util.Collections; -import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 网关设备 TCP 协议集成测试(手动测试) * *

      测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 TCP 协议管理子设备拓扑关系 * - *

      支持两种编解码格式: - *

        - *
      • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
      • - *
      • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
      • - *
      - * *

      使用步骤: *

        *
      1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
      2. - *
      3. 修改 {@link #CODEC} 选择测试的编解码格式
      4. *
      5. 运行以下测试方法: *
          *
        • {@link #testAuth()} - 网关设备认证
        • @@ -66,10 +67,31 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + private static Vertx vertx; + private static NetClient netClient; - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + // ===================== 编解码器 ===================== + + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); + + /** + * TCP 帧编解码器 + */ + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create( + new IotTcpConfig.CodecConfig() + .setType(IotTcpCodecTypeEnum.DELIMITER.getType()) + .setDelimiter("\\n") +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setLengthFieldOffset(0) +// .setLengthFieldLength(4) +// .setLengthAdjustment(0) +// .setInitialBytesToStrip(4) +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setFixedLength(256) + ); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -83,6 +105,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { private static final String SUB_DEVICE_NAME = "chazuo-it"; private static final String SUB_DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + // ===================== 认证测试 ===================== /** @@ -90,29 +131,22 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testAuth][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -123,9 +157,8 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testTopoAdd() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testTopoAdd][认证响应: {}]", authResponse); @@ -140,24 +173,16 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { // 2.2 构建请求参数 IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( + IotDeviceMessage request = IotDeviceMessage.requestOf( IdUtil.fastSimpleUUID(), IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - params, - null, null, null); - // 2.3 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); + params); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoAdd][响应消息: {}]", response); - } else { - log.warn("[testTopoAdd][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testTopoAdd][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -166,35 +191,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testTopoDelete() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testTopoDelete][认证响应: {}]", authResponse); - // 2.1 构建请求参数 + // 2. 构建请求参数 IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); params.setSubDevices(Collections.singletonList( new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); + params); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoDelete][响应消息: {}]", response); - } else { - log.warn("[testTopoDelete][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testTopoDelete][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -203,33 +218,23 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testTopoGet() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testTopoGet][认证响应: {}]", authResponse); - // 2.1 构建请求参数 + // 2. 构建请求参数 IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - params, - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); + params); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoGet][响应消息: {}]", response); - } else { - log.warn("[testTopoGet][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testTopoGet][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -240,35 +245,25 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testSubDeviceRegister() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testSubDeviceRegister][认证响应: {}]", authResponse); - // 2.1 构建请求参数 - IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); - subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); - subDevice.setDeviceName("mougezishebei"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建请求参数 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO() + .setProductKey(SUB_DEVICE_PRODUCT_KEY) + .setDeviceName("mougezishebei"); + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - Collections.singletonList(subDevice), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + Collections.singletonList(subDevice)); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - } else { - log.warn("[testSubDeviceRegister][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -279,9 +274,8 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { */ @Test public void testPropertyPackPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testPropertyPackPost][认证响应: {}]", authResponse); @@ -291,9 +285,9 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { .put("temperature", 25.5) .build(); // 2.2 构建【网关设备】自身事件 - IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - gatewayEvent.setValue(MapUtil.builder().put("message", "gateway started").build()); - gatewayEvent.setTime(System.currentTimeMillis()); + IotDevicePropertyPackPostReqDTO.EventValue gatewayEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("message", "gateway started").build()) + .setTime(System.currentTimeMillis()); Map gatewayEvents = MapUtil.builder() .put("statusReport", gatewayEvent) .build(); @@ -302,97 +296,95 @@ public class IotGatewayDeviceTcpProtocolIntegrationTest { .put("power", 100) .build(); // 2.4 构建【网关子设备】事件 - IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue(); - subDeviceEvent.setValue(MapUtil.builder().put("errorCode", 0).build()); - subDeviceEvent.setTime(System.currentTimeMillis()); + IotDevicePropertyPackPostReqDTO.EventValue subDeviceEvent = new IotDevicePropertyPackPostReqDTO.EventValue() + .setValue(MapUtil.builder().put("errorCode", 0).build()) + .setTime(System.currentTimeMillis()); Map subDeviceEvents = MapUtil.builder() .put("healthCheck", subDeviceEvent) .build(); // 2.5 构建子设备数据 - IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData(); - subDeviceData.setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)); - subDeviceData.setProperties(subDeviceProperties); - subDeviceData.setEvents(subDeviceEvents); + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData = new IotDevicePropertyPackPostReqDTO.SubDeviceData() + .setIdentity(new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME)) + .setProperties(subDeviceProperties) + .setEvents(subDeviceEvents); // 2.6 构建请求参数 IotDevicePropertyPackPostReqDTO params = new IotDevicePropertyPackPostReqDTO(); params.setProperties(gatewayProperties); params.setEvents(gatewayEvents); params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - params, - null, null, null); - // 2.7 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + params); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPackPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPackPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testPropertyPackPost][响应消息: {}]", response); + } finally { + socket.close(); } } // ===================== 辅助方法 ===================== + /** + * 建立 TCP 连接 + * + * @return 连接 Future + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + /** * 执行网关设备认证 * * @param socket TCP 连接 * @return 认证响应消息 */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { + private IotDeviceMessage authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - return CODEC.decode(responseBytes); - } - return null; + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo); + return sendAndReceive(socket, request); } /** - * 发送 TCP 请求并接收响应 + * 发送消息并接收响应(复用 IotTcpFrameCodec 编解码逻辑) * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param socket TCP 连接 + * @param request 请求消息 + * @return 响应消息 */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; + private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception { + // 1. 使用 FRAME_CODEC 创建解码器(复用 gateway 的拆包逻辑) + CompletableFuture responseFuture = new CompletableFuture<>(); + RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> { + try { + // 反序列化响应 + IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes()); + responseFuture.complete(response); + } catch (Exception e) { + responseFuture.completeExceptionally(e); } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } + }); + socket.handler(parser); + + // 2.1 序列化 + 帧编码 + byte[] serializedData = SERIALIZER.serialize(request); + Buffer frameData = FRAME_CODEC.encode(serializedData); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); + // 2.2 发送请求 + socket.write(frameData); + + // 3. 等待响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", + SERIALIZER.serialize(response).length); + return response; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java index c918b474c..22b654a86 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotGatewaySubDeviceTcpProtocolIntegrationTest.java @@ -1,23 +1,31 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; +import io.vertx.core.Vertx; +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 org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * IoT 网关子设备 TCP 协议集成测试(手动测试) @@ -26,17 +34,10 @@ import java.net.Socket; * *

          重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 * - *

          支持两种编解码格式: - *

            - *
          • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
          • - *
          • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
          • - *
          - * *

          使用步骤: *

            *
          1. 启动 yudao-module-iot-gateway 服务(TCP 端口 8091)
          2. *
          3. 确保子设备已通过 {@link IotGatewayDeviceTcpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
          4. - *
          5. 修改 {@link #CODEC} 选择测试的编解码格式
          6. *
          7. 运行以下测试方法: *
              *
            • {@link #testAuth()} - 子设备认证
            • @@ -58,10 +59,31 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { private static final int SERVER_PORT = 8091; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + private static Vertx vertx; + private static NetClient netClient; - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + // ===================== 编解码器 ===================== + + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); + + /** + * TCP 帧编解码器 + */ + private static final IotTcpFrameCodec FRAME_CODEC = IotTcpFrameCodecFactory.create( + new IotTcpConfig.CodecConfig() + .setType(IotTcpCodecTypeEnum.DELIMITER.getType()) + .setDelimiter("\\n") +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setLengthFieldOffset(0) +// .setLengthFieldLength(4) +// .setLengthAdjustment(0) +// .setInitialBytesToStrip(4) +// .setType(IotTcpCodecTypeEnum.LENGTH_FIELD.getType()) +// .setFixedLength(256) + ); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -69,6 +91,25 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { private static final String DEVICE_NAME = "chazuo-it"; private static final String DEVICE_SECRET = "d46ef9b28ab14238b9c00a3a668032af"; + @BeforeAll + static void setUp() { + vertx = Vertx.vertx(); + NetClientOptions options = new NetClientOptions() + .setConnectTimeout(TIMEOUT_MS) + .setIdleTimeout(TIMEOUT_MS); + netClient = vertx.createNetClient(options); + } + + @AfterAll + static void tearDown() { + if (netClient != null) { + netClient.close(); + } + if (vertx != null) { + vertx.close(); + } + } + // ===================== 认证测试 ===================== /** @@ -76,28 +117,21 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - } else { - log.warn("[testAuth][未收到响应]"); - } + // 2. 发送并接收响应 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testAuth][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -108,37 +142,27 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testPropertyPost][认证响应: {}]", authResponse); - // 2.1 构建属性上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("power", 100) .put("status", "online") .put("temperature", 36.5) - .build()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + .build())); log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testPropertyPost][响应消息: {}]", response); + } finally { + socket.close(); } } @@ -149,16 +173,14 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) { - socket.setSoTimeout(TIMEOUT_MS); - + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { // 1. 先进行认证 IotDeviceMessage authResponse = authenticate(socket); log.info("[testEventPost][认证响应: {}]", authResponse); - // 2.1 构建事件上报消息 - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 2. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), IotDeviceEventPostReqDTO.of( "alarm", @@ -168,78 +190,77 @@ public class IotGatewaySubDeviceTcpProtocolIntegrationTest { .put("threshold", 40) .put("current", 42) .build(), - System.currentTimeMillis()), - null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + System.currentTimeMillis())); log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 3.1 发送请求 - byte[] responseBytes = sendAndReceive(socket, payload); - // 3.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } + // 3. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(socket, request); + log.info("[testEventPost][响应消息: {}]", response); + } finally { + socket.close(); } } // ===================== 辅助方法 ===================== + /** + * 建立 TCP 连接 + * + * @return 连接 Future + */ + private CompletableFuture connect() { + CompletableFuture future = new CompletableFuture<>(); + netClient.connect(SERVER_PORT, SERVER_HOST) + .onSuccess(future::complete) + .onFailure(future::completeExceptionally); + return future; + } + /** * 执行子设备认证 * * @param socket TCP 连接 * @return 认证响应消息 */ - private IotDeviceMessage authenticate(Socket socket) throws Exception { + private IotDeviceMessage authenticate(NetSocket socket) throws Exception { IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); - IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() - .setClientId(authInfo.getClientId()) - .setUsername(authInfo.getUsername()) - .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); - byte[] responseBytes = sendAndReceive(socket, payload); - if (responseBytes != null) { - return CODEC.decode(responseBytes); - } - return null; + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authInfo); + return sendAndReceive(socket, request); } /** - * 发送 TCP 请求并接收响应 + * 发送消息并接收响应(复用 IotTcpFrameCodec 编解码逻辑) * - * @param socket TCP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param socket TCP 连接 + * @param request 请求消息 + * @return 响应消息 */ - private byte[] sendAndReceive(Socket socket, byte[] payload) throws Exception { - // 1. 发送请求 - OutputStream out = socket.getOutputStream(); - InputStream in = socket.getInputStream(); - out.write(payload); - out.flush(); - - // 2.1 等待一小段时间让服务器处理 - Thread.sleep(100); - // 2.2 接收响应 - byte[] buffer = new byte[4096]; - try { - int length = in.read(buffer); - if (length > 0) { - byte[] response = new byte[length]; - System.arraycopy(buffer, 0, response, 0, length); - return response; + private IotDeviceMessage sendAndReceive(NetSocket socket, IotDeviceMessage request) throws Exception { + // 1. 使用 FRAME_CODEC 创建解码器(复用 gateway 的拆包逻辑) + CompletableFuture responseFuture = new CompletableFuture<>(); + RecordParser parser = FRAME_CODEC.createDecodeParser(buffer -> { + try { + // 反序列化响应 + IotDeviceMessage response = SERIALIZER.deserialize(buffer.getBytes()); + responseFuture.complete(response); + } catch (Exception e) { + responseFuture.completeExceptionally(e); } - return null; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; - } + }); + socket.handler(parser); + + // 2.1 序列化 + 帧编码 + byte[] serializedData = SERIALIZER.serialize(request); + Buffer frameData = FRAME_CODEC.encode(serializedData); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), frameData.length()); + // 2.2 发送请求 + socket.write(frameData); + + // 3. 等待响应 + IotDeviceMessage response = responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", + SERIALIZER.serialize(response).length); + return response; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java index 9d507cc03..ef7f2ff30 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -9,9 +8,9 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -27,16 +26,9 @@ import java.util.Map; * *

              测试场景:直连设备(IotProductDeviceTypeEnum 的 DIRECT 类型)通过 UDP 协议直接连接平台 * - *

              支持两种编解码格式: - *

                - *
              • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
              • - *
              • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
              • - *
              - * *

              使用步骤: *

                *
              1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
              2. - *
              3. 修改 {@link #CODEC} 选择测试的编解码格式
              4. *
              5. 运行 {@link #testAuth()} 获取设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
              6. *
              7. 运行以下测试方法: *
                  @@ -58,10 +50,12 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { private static final int SERVER_PORT = 8093; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -72,7 +66,7 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { /** * 直连设备 Token:从 {@link #testAuth()} 方法获取后,粘贴到这里 */ - private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTk0ODYzOCwiZGV2aWNlTmFtZSI6InNtYWxsIn0.TrOJisXhloZ3quLBOAIyowmpq6Syp9PHiEpfj-nQ9xo"; + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc3MDUyNTA0MywiZGV2aWNlTmFtZSI6InNtYWxsIn0.W9Mo-Oe1ZNLDkINndKieUeW1XhDzhVp0W0zTAwO6hJM"; // ===================== 认证测试 ===================== @@ -81,30 +75,18 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testAuth][响应消息: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); } // ===================== 动态注册测试 ===================== @@ -118,30 +100,21 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testDeviceRegister() throws Exception { - // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-udp-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + // 1. 构建注册消息 + String deviceName = "test-udp-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName(deviceName) + .setSign(sign); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testDeviceRegister][响应消息: {}]", response); - log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); - } else { - log.warn("[testDeviceRegister][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testDeviceRegister][响应消息: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); } // ===================== 直连设备属性上报测试 ===================== @@ -151,31 +124,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("width", 1) .put("height", "2") - .build())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + .build()))); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testPropertyPost][响应消息: {}]", response); } // ===================== 直连设备事件上报测试 ===================== @@ -185,31 +144,17 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), withToken(IotDeviceEventPostReqDTO.of( "eat", MapUtil.builder().put("rice", 3).build(), - System.currentTimeMillis())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + System.currentTimeMillis()))); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testEventPost][响应消息: {}]", response); } // ===================== 辅助方法 ===================== @@ -232,30 +177,36 @@ public class IotDirectDeviceUdpProtocolIntegrationTest { } /** - * 发送 UDP 请求并接收响应 + * 发送 UDP 消息并接收响应 * - * @param socket UDP Socket - * @param payload 请求数据 - * @return 响应数据 + * @param request 请求消息 + * @return 响应消息 */ - public static byte[] sendAndReceive(DatagramSocket socket, byte[] payload) throws Exception { - InetAddress address = InetAddress.getByName(SERVER_HOST); + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { + // 1. 序列化请求 + byte[] payload = SERIALIZER.serialize(request); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); - // 发送请求 - DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); - socket.send(sendPacket); + // 2. 发送请求 + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + InetAddress address = InetAddress.getByName(SERVER_HOST); + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); + socket.send(sendPacket); - // 接收响应 - byte[] receiveData = new byte[4096]; - DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); - try { - socket.receive(receivePacket); - byte[] response = new byte[receivePacket.getLength()]; - System.arraycopy(receivePacket.getData(), 0, response, 0, receivePacket.getLength()); - return response; - } catch (java.net.SocketTimeoutException e) { - log.warn("[sendAndReceive][接收响应超时]"); - return null; + // 3. 接收响应 + byte[] receiveData = new byte[4096]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + socket.receive(receivePacket); + byte[] responseBytes = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); + log.info("[sendAndReceive][收到响应,数据长度: {} 字节]", responseBytes.length); + return SERIALIZER.deserialize(responseBytes); + } catch (java.net.SocketTimeoutException e) { + log.warn("[sendAndReceive][接收响应超时]"); + return null; + } } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java index 20c2933a0..0acdeae38 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewayDeviceUdpProtocolIntegrationTest.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -13,36 +12,27 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.net.DatagramPacket; import java.net.DatagramSocket; +import java.net.InetAddress; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; - /** * IoT 网关设备 UDP 协议集成测试(手动测试) * *

                  测试场景:网关设备(IotProductDeviceTypeEnum 的 GATEWAY 类型)通过 UDP 协议管理子设备拓扑关系 * - *

                  支持两种编解码格式: - *

                    - *
                  • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
                  • - *
                  • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
                  • - *
                  - * *

                  使用步骤: *

                    *
                  1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
                  2. - *
                  3. 修改 {@link #CODEC} 选择测试的编解码格式
                  4. *
                  5. 运行 {@link #testAuth()} 获取网关设备 token,将返回的 token 粘贴到 {@link #GATEWAY_TOKEN} 常量
                  6. *
                  7. 运行以下测试方法: *
                      @@ -63,12 +53,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd @Disabled public class IotGatewayDeviceUdpProtocolIntegrationTest { + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8093; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -93,185 +87,101 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo( GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testAuth][响应消息: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); } // ===================== 拓扑管理测试 ===================== /** * 添加子设备拓扑关系测试 - *

                      - * 网关设备向平台上报需要绑定的子设备信息 */ @Test public void testTopoAdd() throws Exception { - // 1.1 构建子设备认证信息 + // 1. 构建子设备认证信息 IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo( SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET); IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO() .setClientId(subAuthInfo.getClientId()) .setUsername(subAuthInfo.getUsername()) .setPassword(subAuthInfo.getPassword()); - // 1.2 构建请求参数 IotDeviceTopoAddReqDTO params = new IotDeviceTopoAddReqDTO(); params.setSubDevices(Collections.singletonList(subDeviceAuth)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), - withToken(params), - null, null, null); - // 1.3 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), withToken(params)); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoAdd][响应消息: {}]", response); - } else { - log.warn("[testTopoAdd][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testTopoAdd][响应消息: {}]", response); } /** * 删除子设备拓扑关系测试 - *

                      - * 网关设备向平台上报需要解绑的子设备信息 */ @Test public void testTopoDelete() throws Exception { - // 1.1 构建请求参数 + // 1. 构建请求参数 IotDeviceTopoDeleteReqDTO params = new IotDeviceTopoDeleteReqDTO(); params.setSubDevices(Collections.singletonList( new IotDeviceIdentity(SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME))); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), - withToken(params), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), withToken(params)); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoDelete][响应消息: {}]", response); - } else { - log.warn("[testTopoDelete][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testTopoDelete][响应消息: {}]", response); } /** * 获取子设备拓扑关系测试 - *

                      - * 网关设备向平台查询已绑定的子设备列表 */ @Test public void testTopoGet() throws Exception { - // 1.1 构建请求参数(目前为空,预留扩展) + // 1. 构建请求参数 IotDeviceTopoGetReqDTO params = new IotDeviceTopoGetReqDTO(); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), - withToken(params), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), withToken(params)); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testTopoGet][响应消息: {}]", response); - } else { - log.warn("[testTopoGet][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testTopoGet][响应消息: {}]", response); } // ===================== 子设备注册测试 ===================== /** * 子设备动态注册测试 - *

                      - * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret - *

                      - * 注意:此接口需要网关 Token 认证 */ @Test public void testSubDeviceRegister() throws Exception { - // 1.1 构建请求参数 + // 1. 构建请求参数 IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); subDevice.setDeviceName("mougezishebei"); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), - withToken(Collections.singletonList(subDevice)), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + withToken(Collections.singletonList(subDevice))); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testSubDeviceRegister][响应消息: {}]", response); - } else { - log.warn("[testSubDeviceRegister][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testSubDeviceRegister][响应消息: {}]", response); } // ===================== 批量上报测试 ===================== /** * 批量上报属性测试(网关 + 子设备) - *

                      - * 网关设备批量上报自身属性、事件,以及子设备的属性、事件 */ @Test public void testPropertyPackPost() throws Exception { @@ -307,40 +217,18 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { params.setProperties(gatewayProperties); params.setEvents(gatewayEvents); params.setSubDevices(ListUtil.of(subDeviceData)); - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), - IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), - withToken(params), - null, null, null); - // 1.7 编码 - byte[] payload = CODEC.encode(request); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + IotDeviceMessage request = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), withToken(params)); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPackPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPackPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testPropertyPackPost][响应消息: {}]", response); } // ===================== 辅助方法 ===================== /** * 构建带 token 的 params - *

                      - * 返回格式:{token: "xxx", body: params} - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param params 原始参数(Map、List 或对象) - * @return 包含 token 和 body 的 Map */ private Map withToken(Object params) { Map result = new HashMap<>(); @@ -349,4 +237,31 @@ public class IotGatewayDeviceUdpProtocolIntegrationTest { return result; } + /** + * 发送 UDP 消息并接收响应 + */ + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { + byte[] payload = SERIALIZER.serialize(request); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); + + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + InetAddress address = InetAddress.getByName(SERVER_HOST); + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); + socket.send(sendPacket); + + byte[] receiveData = new byte[4096]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + socket.receive(receivePacket); + byte[] responseBytes = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); + return SERIALIZER.deserialize(responseBytes); + } catch (java.net.SocketTimeoutException e) { + log.warn("[sendAndReceive][接收响应超时]"); + return null; + } + } + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java index 100c276de..fe7f7f812 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotGatewaySubDeviceUdpProtocolIntegrationTest.java @@ -1,26 +1,24 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.net.DatagramPacket; import java.net.DatagramSocket; +import java.net.InetAddress; import java.util.HashMap; import java.util.Map; -import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUdpProtocolIntegrationTest.sendAndReceive; - /** * IoT 网关子设备 UDP 协议集成测试(手动测试) * @@ -29,17 +27,10 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd *

                      重要说明:子设备无法直接连接平台,所有请求均由网关设备(Gateway)代为转发。 *

                      网关设备转发子设备请求时,Token 使用子设备自己的信息。 * - *

                      支持两种编解码格式: - *

                        - *
                      • {@link IotTcpJsonDeviceMessageCodec} - JSON 格式
                      • - *
                      • {@link IotTcpBinaryDeviceMessageCodec} - 二进制格式
                      • - *
                      - * *

                      使用步骤: *

                        *
                      1. 启动 yudao-module-iot-gateway 服务(UDP 端口 8093)
                      2. *
                      3. 确保子设备已通过 {@link IotGatewayDeviceUdpProtocolIntegrationTest#testTopoAdd()} 绑定到网关
                      4. - *
                      5. 修改 {@link #CODEC} 选择测试的编解码格式
                      6. *
                      7. 运行 {@link #testAuth()} 获取子设备 token,将返回的 token 粘贴到 {@link #TOKEN} 常量
                      8. *
                      9. 运行以下测试方法: *
                          @@ -57,12 +48,16 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotDirectDeviceUd @Disabled public class IotGatewaySubDeviceUdpProtocolIntegrationTest { + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8093; private static final int TIMEOUT_MS = 5000; - // ===================== 编解码器选择(修改此处切换 JSON / Binary) ===================== + // ===================== 序列化器 ===================== - private static final IotDeviceMessageCodec CODEC = new IotTcpJsonDeviceMessageCodec(); -// private static final IotDeviceMessageCodec CODEC = new IotTcpBinaryDeviceMessageCodec(); + /** + * 消息序列化器 + */ + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -82,30 +77,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testAuth() throws Exception { - // 1.1 构建认证消息 + // 1. 构建认证消息 IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET); IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() .setClientId(authInfo.getClientId()) .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); - IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); - log.info("[testAuth][Codec: {}, 请求消息: {}, 数据包长度: {} 字节]", CODEC.type(), request, payload.length); + IotDeviceMessage request = IotDeviceMessage.requestOf("auth", authReqDTO); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testAuth][响应消息: {}]", response); - log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); - } else { - log.warn("[testAuth][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testAuth][响应消息: {}]", response); + log.info("[testAuth][请将返回的 token 复制到 TOKEN 常量中]"); } // ===================== 子设备属性上报测试 ===================== @@ -115,33 +98,19 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testPropertyPost() throws Exception { - // 1.1 构建属性上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建属性上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), withToken(IotDevicePropertyPostReqDTO.of(MapUtil.builder() .put("power", 100) .put("status", "online") .put("temperature", 36.5) - .build())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + .build()))); log.info("[testPropertyPost][子设备属性上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testPropertyPost][响应消息: {}]", response); - } else { - log.warn("[testPropertyPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testPropertyPost][响应消息: {}]", response); } // ===================== 子设备事件上报测试 ===================== @@ -151,9 +120,8 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { */ @Test public void testEventPost() throws Exception { - // 1.1 构建事件上报消息(UDP 协议:token 放在 params 中) - IotDeviceMessage request = IotDeviceMessage.of( - IdUtil.fastSimpleUUID(), + // 1. 构建事件上报消息 + IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), withToken(IotDeviceEventPostReqDTO.of( "alarm", @@ -163,38 +131,18 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { .put("threshold", 40) .put("current", 42) .build(), - System.currentTimeMillis())), - null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + System.currentTimeMillis()))); log.info("[testEventPost][子设备事件上报 - 请求实际由 Gateway 代为转发]"); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); - // 2.1 发送请求 - try (DatagramSocket socket = new DatagramSocket()) { - socket.setSoTimeout(TIMEOUT_MS); - byte[] responseBytes = sendAndReceive(socket, payload); - // 2.2 解码响应 - if (responseBytes != null) { - IotDeviceMessage response = CODEC.decode(responseBytes); - log.info("[testEventPost][响应消息: {}]", response); - } else { - log.warn("[testEventPost][未收到响应]"); - } - } + // 2. 发送并接收响应 + IotDeviceMessage response = sendAndReceive(request); + log.info("[testEventPost][响应消息: {}]", response); } // ===================== 辅助方法 ===================== /** * 构建带 token 的 params - *

                          - * 返回格式:{token: "xxx", body: params} - * - token:JWT 令牌 - * - body:实际请求内容(可以是 Map、List 或其他类型) - * - * @param params 原始参数(Map、List 或对象) - * @return 包含 token 和 body 的 Map */ private Map withToken(Object params) { Map result = new HashMap<>(); @@ -203,4 +151,31 @@ public class IotGatewaySubDeviceUdpProtocolIntegrationTest { return result; } + /** + * 发送 UDP 消息并接收响应 + */ + private IotDeviceMessage sendAndReceive(IotDeviceMessage request) throws Exception { + byte[] payload = SERIALIZER.serialize(request); + log.info("[sendAndReceive][发送消息: {},数据长度: {} 字节]", request.getMethod(), payload.length); + + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(TIMEOUT_MS); + InetAddress address = InetAddress.getByName(SERVER_HOST); + DatagramPacket sendPacket = new DatagramPacket(payload, payload.length, address, SERVER_PORT); + socket.send(sendPacket); + + byte[] receiveData = new byte[4096]; + DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); + try { + socket.receive(receivePacket); + byte[] responseBytes = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), 0, responseBytes, 0, receivePacket.getLength()); + return SERIALIZER.deserialize(responseBytes); + } catch (java.net.SocketTimeoutException e) { + log.warn("[sendAndReceive][接收响应超时]"); + return null; + } + } + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java index ca79c4220..ba80ed1ed 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java @@ -10,8 +10,9 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; @@ -61,7 +62,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { // ===================== 编解码器选择 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 直连设备信息(根据实际情况修改,从 iot_device 表查询) ===================== @@ -95,10 +96,10 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -109,7 +110,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testAuth][响应消息: {}]", responseMessage); } else { log.warn("[testAuth][未收到响应]"); @@ -131,16 +132,19 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { @Test public void testDeviceRegister() throws Exception { // 1.1 构建注册消息 - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); - registerReqDTO.setProductKey(PRODUCT_KEY); - registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis()); - registerReqDTO.setProductSecret("test-product-secret"); + String deviceName = "test-ws-" + System.currentTimeMillis(); + String productSecret = "test-product-secret"; // 替换为实际的 productSecret + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret); + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(PRODUCT_KEY) + .setDeviceName(deviceName) + .setSign(sign); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testDeviceRegister][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -151,7 +155,7 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testDeviceRegister][响应消息: {}]", responseMessage); log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); } else { @@ -186,16 +190,16 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { .put("height", "2") .build()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testPropertyPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testPropertyPost][响应消息: {}]", responseMessage); } else { log.warn("[testPropertyPost][未收到响应]"); @@ -229,16 +233,16 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { MapUtil.builder().put("rice", 3).build(), System.currentTimeMillis()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testEventPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testEventPost][响应消息: {}]", responseMessage); } else { log.warn("[testEventPost][未收到响应]"); @@ -308,13 +312,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest { .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); log.info("[authenticate][发送认证请求: {}]", jsonMessage); String response = sendAndReceive(ws, jsonMessage); if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); + return SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); } return null; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java index a44f2f6dd..20d66fa0a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewayDeviceWebSocketProtocolIntegrationTest.java @@ -14,8 +14,8 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; @@ -67,9 +67,9 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器选择 ===================== + // ===================== 序列化器选择 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关设备信息(根据实际情况修改,从 iot_device 表查询网关设备) ===================== @@ -110,10 +110,10 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -124,7 +124,7 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testAuth][响应消息: {}]", responseMessage); } else { log.warn("[testAuth][未收到响应]"); @@ -164,16 +164,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.TOPO_ADD.getMethod(), params, null, null, null); - // 2.3 编码 - byte[] payload = CODEC.encode(request); + // 2.3 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoAdd][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testTopoAdd][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testTopoAdd][响应消息: {}]", responseMessage); } else { log.warn("[testTopoAdd][未收到响应]"); @@ -205,16 +205,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod(), params, null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoDelete][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testTopoDelete][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testTopoDelete][响应消息: {}]", responseMessage); } else { log.warn("[testTopoDelete][未收到响应]"); @@ -244,16 +244,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.TOPO_GET.getMethod(), params, null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testTopoGet][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testTopoGet][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testTopoGet][响应消息: {}]", responseMessage); } else { log.warn("[testTopoGet][未收到响应]"); @@ -287,16 +287,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod(), Collections.singletonList(subDevice), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testSubDeviceRegister][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testSubDeviceRegister][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testSubDeviceRegister][响应消息: {}]", responseMessage); } else { log.warn("[testSubDeviceRegister][未收到响应]"); @@ -358,16 +358,16 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod(), params, null, null, null); - // 2.7 编码 - byte[] payload = CODEC.encode(request); + // 2.7 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPackPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testPropertyPackPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testPropertyPackPost][响应消息: {}]", responseMessage); } else { log.warn("[testPropertyPackPost][未收到响应]"); @@ -438,13 +438,13 @@ public class IotGatewayDeviceWebSocketProtocolIntegrationTest { .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); log.info("[authenticate][发送认证请求: {}]", jsonMessage); String response = sendAndReceive(ws, jsonMessage); if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); + return SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); } return null; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java index 04bf3d563..f792288fe 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotGatewaySubDeviceWebSocketProtocolIntegrationTest.java @@ -9,8 +9,8 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import io.vertx.core.Vertx; import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; @@ -60,9 +60,9 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { private static Vertx vertx; - // ===================== 编解码器选择 ===================== + // ===================== 序列化器选择 ===================== - private static final IotDeviceMessageCodec CODEC = new IotAlinkDeviceMessageCodec(); + private static final IotMessageSerializer SERIALIZER = new IotJsonSerializer(); // ===================== 网关子设备信息(根据实际情况修改,从 iot_device 表查询子设备) ===================== @@ -96,10 +96,10 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .setUsername(authInfo.getUsername()) .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - // 1.2 编码 - byte[] payload = CODEC.encode(request); + // 1.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testAuth][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testAuth][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 2.1 创建 WebSocket 连接(同步) WebSocket ws = createWebSocketConnection(); @@ -110,7 +110,7 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { // 3. 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testAuth][响应消息: {}]", responseMessage); } else { log.warn("[testAuth][未收到响应]"); @@ -146,16 +146,16 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .put("temperature", 36.5) .build()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testPropertyPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testPropertyPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testPropertyPost][响应消息: {}]", responseMessage); } else { log.warn("[testPropertyPost][未收到响应]"); @@ -195,16 +195,16 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .build(), System.currentTimeMillis()), null, null, null); - // 2.2 编码 - byte[] payload = CODEC.encode(request); + // 2.2 序列化 + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); - log.info("[testEventPost][Codec: {}, 请求消息: {}]", CODEC.type(), request); + log.info("[testEventPost][Serialize: {}, 请求消息: {}]", SERIALIZER.getType(), request); // 3.1 发送并等待响应 String response = sendAndReceive(ws, jsonMessage); // 3.2 解码响应 if (response != null) { - IotDeviceMessage responseMessage = CODEC.decode(StrUtil.utf8Bytes(response)); + IotDeviceMessage responseMessage = SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); log.info("[testEventPost][响应消息: {}]", responseMessage); } else { log.warn("[testEventPost][未收到响应]"); @@ -274,13 +274,13 @@ public class IotGatewaySubDeviceWebSocketProtocolIntegrationTest { .setPassword(authInfo.getPassword()); IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), "auth", authReqDTO, null, null, null); - byte[] payload = CODEC.encode(request); + byte[] payload = SERIALIZER.serialize(request); String jsonMessage = StrUtil.utf8Str(payload); log.info("[authenticate][发送认证请求: {}]", jsonMessage); String response = sendAndReceive(ws, jsonMessage); if (response != null) { - return CODEC.decode(StrUtil.utf8Bytes(response)); + return SERIALIZER.deserialize(StrUtil.utf8Bytes(response)); } return null; } diff --git a/yudao-module-iot/yudao-module-iot-server/pom.xml b/yudao-module-iot/yudao-module-iot-server/pom.xml index 76461c2e3..35acffcb8 100644 --- a/yudao-module-iot/yudao-module-iot-server/pom.xml +++ b/yudao-module-iot/yudao-module-iot-server/pom.xml @@ -153,18 +153,6 @@ true - - - - - - - - - - - - diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index db0a862d0..7922eb1b2 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -1,31 +1,41 @@ package cn.iocoder.yudao.module.iot.api.device; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.RpcConstants; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.*; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Set; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; /** * IoT 设备 API 实现类 @@ -41,6 +51,12 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { private IotDeviceService deviceService; @Resource private IotProductService productService; + @Resource + @Lazy // 延迟加载,解决循环依赖 + private IotDeviceModbusConfigService modbusConfigService; + @Resource + @Lazy // 延迟加载,解决循环依赖 + private IotDeviceModbusPointService modbusPointService; @Override @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") @@ -58,11 +74,57 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> { IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId()); if (product != null) { - deviceDTO.setCodecType(product.getCodecType()); + deviceDTO.setProtocolType(product.getProtocolType()).setSerializeType(product.getSerializeType()); } })); } + @Override + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/config-list") + @PermitAll + @TenantIgnore + public CommonResult> getModbusDeviceConfigList( + @RequestBody IotModbusDeviceConfigListReqDTO listReqDTO) { + // 1. 获取 Modbus 连接配置 + List configList = modbusConfigService.getDeviceModbusConfigList(listReqDTO); + if (CollUtil.isEmpty(configList)) { + return success(new ArrayList<>()); + } + + // 2. 组装返回结果 + Set deviceIds = convertSet(configList, IotDeviceModbusConfigDO::getDeviceId); + Map deviceMap = deviceService.getDeviceMap(deviceIds); + Map> pointMap = modbusPointService.getEnabledDeviceModbusPointMapByDeviceIds(deviceIds); + Map productMap = productService.getProductMap(convertSet(deviceMap.values(), IotDeviceDO::getProductId)); + List result = new ArrayList<>(configList.size()); + for (IotDeviceModbusConfigDO config : configList) { + // 3.1 获取设备信息 + IotDeviceDO device = deviceMap.get(config.getDeviceId()); + if (device == null) { + continue; + } + // 3.2 按 protocolType 筛选(如果非空) + if (StrUtil.isNotEmpty(listReqDTO.getProtocolType())) { + IotProductDO product = productMap.get(device.getProductId()); + if (product == null || ObjUtil.notEqual(listReqDTO.getProtocolType(), product.getProtocolType())) { + continue; + } + } + // 3.3 获取启用的点位列表 + List pointList = pointMap.get(config.getDeviceId()); + if (CollUtil.isEmpty(pointList)) { + continue; + } + + // 3.4 构建 IotModbusDeviceConfigRespDTO 对象 + IotModbusDeviceConfigRespDTO configDTO = BeanUtils.toBean(config, IotModbusDeviceConfigRespDTO.class, o -> + o.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()) + .setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class))); + result.add(configDTO); + } + return success(result); + } + @Override @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register") @PermitAll diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java new file mode 100644 index 000000000..576d904ac --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 设备 Modbus 连接配置") +@RestController +@RequestMapping("/iot/device-modbus-config") +@Validated +public class IotDeviceModbusConfigController { + + @Resource + private IotDeviceModbusConfigService modbusConfigService; + + @PostMapping("/save") + @Operation(summary = "保存设备 Modbus 连接配置") + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult saveDeviceModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO saveReqVO) { + modbusConfigService.saveDeviceModbusConfig(saveReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得设备 Modbus 连接配置") + @Parameter(name = "id", description = "编号", example = "1024") + @Parameter(name = "deviceId", description = "设备编号", example = "2048") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult getDeviceModbusConfig( + @RequestParam(value = "id", required = false) Long id, + @RequestParam(value = "deviceId", required = false) Long deviceId) { + IotDeviceModbusConfigDO modbusConfig = null; + if (id != null) { + modbusConfig = modbusConfigService.getDeviceModbusConfig(id); + } else if (deviceId != null) { + modbusConfig = modbusConfigService.getDeviceModbusConfigByDeviceId(deviceId); + } + return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java new file mode 100644 index 000000000..4e813d8bf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 设备 Modbus 点位配置") +@RestController +@RequestMapping("/iot/device-modbus-point") +@Validated +public class IotDeviceModbusPointController { + + @Resource + private IotDeviceModbusPointService modbusPointService; + + @PostMapping("/create") + @Operation(summary = "创建设备 Modbus 点位配置") + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult createDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) { + return success(modbusPointService.createDeviceModbusPoint(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新设备 Modbus 点位配置") + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult updateDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) { + modbusPointService.updateDeviceModbusPoint(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除设备 Modbus 点位配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult deleteDeviceModbusPoint(@RequestParam("id") Long id) { + modbusPointService.deleteDeviceModbusPoint(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得设备 Modbus 点位配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult getDeviceModbusPoint(@RequestParam("id") Long id) { + IotDeviceModbusPointDO modbusPoint = modbusPointService.getDeviceModbusPoint(id); + return success(BeanUtils.toBean(modbusPoint, IotDeviceModbusPointRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得设备 Modbus 点位配置分页") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult> getDeviceModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) { + PageResult pageResult = modbusPointService.getDeviceModbusPointPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceModbusPointRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java index e527242fb..ddedb6135 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java index e53f5acb6..f9e4b7529 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java @@ -38,7 +38,7 @@ public class IotDeviceMessageRespVO { @Schema(description = "请求编号", example = "req_123") private String requestId; - @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.report") + @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.post") private String method; @Schema(description = "请求参数") diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java new file mode 100644 index 000000000..ecce04de6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置 Response VO") +@Data +public class IotDeviceModbusConfigRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long deviceId; + + @Schema(description = "设备名称", example = "温湿度传感器") + private String deviceName; + + @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100") + private String ip; + + @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502") + private Integer port; + + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer slaveId; + + @Schema(description = "连接超时时间(毫秒)", example = "3000") + private Integer timeout; + + @Schema(description = "重试间隔(毫秒)", example = "1000") + private Integer retryInterval; + + @Schema(description = "工作模式", example = "1") + private Integer mode; + + @Schema(description = "数据帧格式", example = "1") + private Integer frameFormat; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java new file mode 100644 index 000000000..f7f26dd42 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置新增/修改 Request VO") +@Data +public class IotDeviceModbusConfigSaveReqVO { + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + + @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100") + private String ip; + + @Schema(description = "Modbus 端口", example = "502") + private Integer port; + + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "从站地址不能为空") + private Integer slaveId; + + @Schema(description = "连接超时时间(毫秒)", example = "3000") + private Integer timeout; + + @Schema(description = "重试间隔(毫秒)", example = "1000") + private Integer retryInterval; + + @Schema(description = "工作模式", example = "1") + @InEnum(IotModbusModeEnum.class) + private Integer mode; + + @Schema(description = "数据帧格式", example = "1") + @InEnum(IotModbusFrameFormatEnum.class) + private Integer frameFormat; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java new file mode 100644 index 000000000..344e8ce12 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IotDeviceModbusPointPageReqVO extends PageParam { + + @Schema(description = "设备编号", example = "1024") + private Long deviceId; + + @Schema(description = "属性标识符", example = "temperature") + private String identifier; + + @Schema(description = "属性名称", example = "温度") + private String name; + + @Schema(description = "Modbus 功能码", example = "3") + private Integer functionCode; + + @Schema(description = "状态", example = "0") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java new file mode 100644 index 000000000..c590e3e3f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置 Response VO") +@Data +public class IotDeviceModbusPointRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long deviceId; + + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long thingModelId; + + @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") + private String identifier; + + @Schema(description = "属性名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度") + private String name; + + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer functionCode; + + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer registerAddress; + + @Schema(description = "寄存器数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer registerCount; + + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB") + private String byteOrder; + + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16") + private String rawDataType; + + @Schema(description = "缩放因子", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0") + private BigDecimal scale; + + @Schema(description = "轮询间隔(毫秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5000") + private Integer pollInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java new file mode 100644 index 000000000..18aea2bf6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置新增/修改 Request VO") +@Data +public class IotDeviceModbusPointSaveReqVO { + + @Schema(description = "主键", example = "1") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "物模型属性编号不能为空") + private Long thingModelId; + + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "Modbus 功能码不能为空") + private Integer functionCode; + + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "寄存器起始地址不能为空") + private Integer registerAddress; + + @Schema(description = "寄存器数量", example = "1") + private Integer registerCount; + + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB") + @NotEmpty(message = "字节序不能为空") + private String byteOrder; + + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16") + @NotEmpty(message = "原始数据类型不能为空") + private String rawDataType; + + @Schema(description = "缩放因子", example = "1.0") + private BigDecimal scale; + + @Schema(description = "轮询间隔(毫秒)", example = "5000") + private Integer pollInterval; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskRecordController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskRecordController.java index 6e26dc66d..3c289926f 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskRecordController.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskRecordController.java @@ -21,11 +21,7 @@ import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.Map; @@ -48,8 +44,8 @@ public class IotOtaTaskRecordController { @GetMapping("/get-status-statistics") @Operation(summary = "获得 OTA 升级记录状态统计") @Parameters({ - @Parameter(name = "firmwareId", description = "固件编号", example = "1024"), - @Parameter(name = "taskId", description = "升级任务编号", example = "2048") + @Parameter(name = "firmwareId", description = "固件编号", example = "1024"), + @Parameter(name = "taskId", description = "升级任务编号", example = "2048") }) @PreAuthorize("@ss.hasPermission('iot:ota-task-record:query')") public CommonResult> getOtaTaskRecordStatusStatistics( @@ -68,17 +64,17 @@ public class IotOtaTaskRecordController { return success(PageResult.empty()); } - // 批量查询固件信息 - Map firmwareMap = otaFirmwareService.getOtaFirmwareMap( - convertSet(pageResult.getList(), IotOtaTaskRecordDO::getFromFirmwareId)); + // 批量查询固件信息 + Map firmwareMap = otaFirmwareService.getOtaFirmwareMap( + convertSet(pageResult.getList(), IotOtaTaskRecordDO::getFromFirmwareId)); Map deviceMap = deviceService.getDeviceMap( - convertSet(pageResult.getList(), IotOtaTaskRecordDO::getDeviceId)); + convertSet(pageResult.getList(), IotOtaTaskRecordDO::getDeviceId)); // 转换为响应 VO return success(BeanUtils.toBean(pageResult, IotOtaTaskRecordRespVO.class, (vo) -> { MapUtils.findAndThen(firmwareMap, vo.getFromFirmwareId(), firmware -> - vo.setFromFirmwareVersion(firmware.getVersion())); + vo.setFromFirmwareVersion(firmware.getVersion())); MapUtils.findAndThen(deviceMap, vo.getDeviceId(), device -> - vo.setDeviceName(device.getDeviceName())); + vo.setDeviceName(device.getDeviceName())); })); } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http new file mode 100644 index 000000000..e4e258985 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http @@ -0,0 +1,5 @@ +### 请求 /iot/product/sync-property-table 接口 => 成功 +POST {{baseUrl}}/iot/product/sync-property-table +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java index 043f48772..130c45d6a 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java @@ -141,6 +141,14 @@ public class IotProductController { result.getData().getList()); } + @PostMapping("/sync-property-table") + @Operation(summary = "同步产品属性表结构到 TDengine") + @PreAuthorize("@ss.hasPermission('iot:product:update')") + public CommonResult syncProductPropertyTable() { + productService.syncProductPropertyTable(); + return success(true); + } + @GetMapping("/simple-list") @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项") @Parameter(name = "deviceType", description = "设备类型", example = "1") diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java index ffc92a213..d2e7cd9c9 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; -import cn.idev.excel.annotation.ExcelIgnoreUnannotated; -import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -67,10 +67,15 @@ public class IotProductRespVO { @DictFormat(DictTypeConstants.NET_TYPE) private Integer netType; - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @ExcelProperty(value = "数据格式", converter = DictConvert.class) - @DictFormat(DictTypeConstants.CODEC_TYPE) - private String codecType; + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") + @ExcelProperty(value = "协议类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PROTOCOL_TYPE) + private String protocolType; + + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") + @ExcelProperty(value = "序列化类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SERIALIZE_TYPE) + private String serializeType; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java index 08c636f7f..fceede0eb 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; @@ -44,9 +46,15 @@ public class IotProductSaveReqVO { @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") private Integer netType; - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @NotEmpty(message = "数据格式不能为空") - private String codecType; + @Schema(description = "协议类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "mqtt") + @InEnum(value = IotProtocolTypeEnum.class, message = "协议类型必须是 {value}") + @NotEmpty(message = "协议类型不能为空") + private String protocolType; + + @Schema(description = "序列化类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "json") + @InEnum(value = IotSerializeTypeEnum.class, message = "序列化类型必须是 {value}") + @NotEmpty(message = "序列化类型不能为空") + private String serializeType; @Schema(description = "是否开启动态注册", example = "false") @NotNull(message = "是否开启动态注册不能为空") diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java index 22837c48b..539ebbfc2 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java @@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java deleted file mode 100644 index feed3eb2a..000000000 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java +++ /dev/null @@ -1,168 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.mq.message; - -import cn.hutool.core.map.MapUtil; -import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * IoT 设备消息 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class IotDeviceMessage { - - /** - * 【消息总线】应用的设备消息 Topic,由 iot-gateway 发给 iot-biz 进行消费 - */ - public static final String MESSAGE_BUS_DEVICE_MESSAGE_TOPIC = "iot_device_message"; - - /** - * 【消息总线】设备消息 Topic,由 iot-biz 发送给 iot-gateway 的某个 "server"(protocol) 进行消费 - * - * 其中,%s 就是该"server"(protocol) 的标识 - */ - public static final String MESSAGE_BUS_GATEWAY_DEVICE_MESSAGE_TOPIC = MESSAGE_BUS_DEVICE_MESSAGE_TOPIC + "_%s"; - - /** - * 消息编号 - * - * 由后端生成,通过 {@link IotDeviceMessageUtils#generateMessageId()} - */ - private String id; - /** - * 上报时间 - * - * 由后端生成,当前时间 - */ - private LocalDateTime reportTime; - - /** - * 设备编号 - */ - private Long deviceId; - /** - * 租户编号 - */ - private Long tenantId; - - /** - * 服务编号,该消息由哪个 server 发送 - */ - private String serverId; - - // ========== codec(编解码)字段 ========== - - /** - * 请求编号 - * - * 由设备生成,对应阿里云 IoT 的 Alink 协议中的 id、华为云 IoTDA 协议的 request_id - */ - private String requestId; - /** - * 请求方法 - * - * 枚举 {@link IotDeviceMessageMethodEnum} - * 例如说:thing.property.report 属性上报 - */ - private String method; - /** - * 请求参数 - * - * 例如说:属性上报的 properties、事件上报的 params - */ - private Object params; - /** - * 响应结果 - */ - private Object data; - /** - * 响应错误码 - */ - private Integer code; - /** - * 返回结果信息 - */ - private String msg; - - // ========== 基础方法:只传递"codec(编解码)字段" ========== - - public static IotDeviceMessage requestOf(String method) { - return requestOf(null, method, null); - } - - public static IotDeviceMessage requestOf(String method, Object params) { - return requestOf(null, method, params); - } - - public static IotDeviceMessage requestOf(String requestId, String method, Object params) { - return of(requestId, method, params, null, null, null); - } - - /** - * 创建设备请求消息(包含设备信息) - * - * @param deviceId 设备编号 - * @param tenantId 租户编号 - * @param serverId 服务标识 - * @param method 消息方法 - * @param params 消息参数 - * @return 消息对象 - */ - public static IotDeviceMessage requestOf(Long deviceId, Long tenantId, String serverId, - String method, Object params) { - IotDeviceMessage message = of(null, method, params, null, null, null); - return message.setId(IotDeviceMessageUtils.generateMessageId()) - .setDeviceId(deviceId).setTenantId(tenantId).setServerId(serverId); - } - - public static IotDeviceMessage replyOf(String requestId, String method, - Object data, Integer code, String msg) { - if (code == null) { - code = GlobalErrorCodeConstants.SUCCESS.getCode(); - msg = GlobalErrorCodeConstants.SUCCESS.getMsg(); - } - return of(requestId, method, null, data, code, msg); - } - - public static IotDeviceMessage of(String requestId, String method, - Object params, Object data, Integer code, String msg) { - // 通用参数 - IotDeviceMessage message = new IotDeviceMessage() - .setId(IotDeviceMessageUtils.generateMessageId()).setReportTime(LocalDateTime.now()); - // 当前参数 - message.setRequestId(requestId).setMethod(method).setParams(params) - .setData(data).setCode(code).setMsg(msg); - return message; - } - - // ========== 核心方法:在 of 基础方法之上,添加对应 method ========== - - public static IotDeviceMessage buildStateUpdateOnline() { - return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), - MapUtil.of("state", IotDeviceStateEnum.ONLINE.getState())); - } - - public static IotDeviceMessage buildStateOffline() { - return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), - MapUtil.of("state", IotDeviceStateEnum.OFFLINE.getState())); - } - - public static IotDeviceMessage buildOtaUpgrade(String version, String fileUrl, Long fileSize, - String fileDigestAlgorithm, String fileDigestValue) { - return requestOf(IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), MapUtil.builder() - .put("version", version).put("fileUrl", fileUrl).put("fileSize", fileSize) - .put("fileDigestAlgorithm", fileDigestAlgorithm).put("fileDigestValue", fileDigestValue) - .build()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java deleted file mode 100644 index 609d0a60a..000000000 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -package cn.iocoder.yudao.module.iot.core.util; - -import cn.hutool.core.util.StrUtil; -import cn.hutool.crypto.digest.DigestUtil; -import cn.hutool.crypto.digest.HmacAlgorithm; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; -import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; - -/** - * IoT 设备【认证】的工具类,参考阿里云 - * - * @see 如何计算 MQTT 签名参数 - */ -public class IotDeviceAuthUtils { - - public static IotDeviceAuthReqDTO getAuthInfo(String productKey, String deviceName, String deviceSecret) { - String clientId = buildClientId(productKey, deviceName); - String username = buildUsername(productKey, deviceName); - String password = buildPassword(deviceSecret, - buildContent(clientId, productKey, deviceName, deviceSecret)); - return new IotDeviceAuthReqDTO(clientId, username, password); - } - - public static String buildClientId(String productKey, String deviceName) { - return String.format("%s.%s", productKey, deviceName); - } - - public static String buildUsername(String productKey, String deviceName) { - return String.format("%s&%s", deviceName, productKey); - } - - public static String buildPassword(String deviceSecret, String content) { - return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(deviceSecret)) - .digestHex(content); - } - - private static String buildContent(String clientId, String productKey, String deviceName, String deviceSecret) { - return "clientId" + clientId + - "deviceName" + deviceName + - "deviceSecret" + deviceSecret + - "productKey" + productKey; - } - - public static IotDeviceIdentity parseUsername(String username) { - String[] usernameParts = username.split("&"); - if (usernameParts.length != 2) { - return null; - } - return new IotDeviceIdentity(usernameParts[1], usernameParts[0]); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java index 7b7d021c3..feaf61540 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java @@ -2,14 +2,17 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -108,10 +111,6 @@ public class IotDeviceDO extends TenantBaseDO { */ private LocalDateTime activeTime; - /** - * 设备的 IP 地址 - */ - private String ip; /** * 固件编号 * diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java index 9f1f6a6a0..233b2c140 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java @@ -84,7 +84,7 @@ public class IotDeviceMessageDO { * 请求方法 * * 枚举 {@link IotDeviceMessageMethodEnum} - * 例如说:thing.property.report 属性上报 + * 例如说:thing.property.post 属性上报 */ private String method; /** diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java new file mode 100644 index 000000000..b183b3c3b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备 Modbus 连接配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_device_modbus_config") +@KeySequence("iot_device_modbus_config_seq") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceModbusConfigDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 产品编号 + * + * 关联 {@link IotProductDO#getId()} + */ + private Long productId; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + + /** + * Modbus 服务器 IP 地址 + */ + private String ip; + /** + * Modbus 服务器端口 + */ + private Integer port; + /** + * 从站地址 + */ + private Integer slaveId; + /** + * 连接超时时间,单位:毫秒 + */ + private Integer timeout; + /** + * 重试间隔,单位:毫秒 + */ + private Integer retryInterval; + /** + * 模式 + * + * @see IotModbusModeEnum + */ + private Integer mode; + /** + * 数据帧格式 + * + * @see IotModbusFrameFormatEnum + */ + private Integer frameFormat; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java new file mode 100644 index 000000000..dd3e7bb07 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * IoT 设备 Modbus 点位配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_device_modbus_point") +@KeySequence("iot_device_modbus_point_seq") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceModbusPointDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + /** + * 物模型属性编号 + * + * 关联 {@link IotThingModelDO#getId()} + */ + private Long thingModelId; + /** + * 属性标识符 + * + * 冗余 {@link IotThingModelDO#getIdentifier()} + */ + private String identifier; + /** + * 属性名称 + * + * 冗余 {@link IotThingModelDO#getName()} + */ + private String name; + + // ========== Modbus 协议配置 ========== + + /** + * Modbus 功能码 + * + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器) + */ + private Integer functionCode; + /** + * 寄存器起始地址 + */ + private Integer registerAddress; + /** + * 寄存器数量 + */ + private Integer registerCount; + /** + * 字节序 + * + * 枚举 {@link IotModbusByteOrderEnum} + */ + private String byteOrder; + /** + * 原始数据类型 + * + * 枚举 {@link IotModbusRawDataTypeEnum} + */ + private String rawDataType; + /** + * 缩放因子 + */ + private BigDecimal scale; + /** + * 轮询间隔(毫秒) + */ + private Integer pollInterval; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java index e296b3501..352e43e6a 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java @@ -4,7 +4,10 @@ import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; /** * IoT 产品 DO @@ -78,12 +81,16 @@ public class IotProductDO extends TenantBaseDO { */ private Integer netType; /** - * 数据格式(编解码器类型) + * 协议类型 *

                          - * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE} - * - * 目的:用于 gateway-server 解析消息格式 + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum} */ - private String codecType; + private String protocolType; + /** + * 序列化类型 + *

                          + * 枚举 {@link cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum} + */ + private String serializeType; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java new file mode 100644 index 000000000..b18769c6a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.device; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 设备 Modbus 连接配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDeviceModbusConfigMapper extends BaseMapperX { + + default IotDeviceModbusConfigDO selectByDeviceId(Long deviceId) { + return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId); + } + + default List selectList(IotModbusDeviceConfigListReqDTO reqDTO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqDTO.getStatus()) + .eqIfPresent(IotDeviceModbusConfigDO::getMode, reqDTO.getMode()) + .inIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqDTO.getDeviceIds())); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java new file mode 100644 index 000000000..7c9b5d3ba --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * IoT 设备 Modbus 点位配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDeviceModbusPointMapper extends BaseMapperX { + + default PageResult selectPage(IotDeviceModbusPointPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotDeviceModbusPointDO::getDeviceId, reqVO.getDeviceId()) + .likeIfPresent(IotDeviceModbusPointDO::getIdentifier, reqVO.getIdentifier()) + .likeIfPresent(IotDeviceModbusPointDO::getName, reqVO.getName()) + .eqIfPresent(IotDeviceModbusPointDO::getFunctionCode, reqVO.getFunctionCode()) + .eqIfPresent(IotDeviceModbusPointDO::getStatus, reqVO.getStatus()) + .orderByDesc(IotDeviceModbusPointDO::getId)); + } + + default List selectListByDeviceIdsAndStatus(Collection deviceIds, Integer status) { + return selectList(new LambdaQueryWrapperX() + .in(IotDeviceModbusPointDO::getDeviceId, deviceIds) + .eq(IotDeviceModbusPointDO::getStatus, status)); + } + + default IotDeviceModbusPointDO selectByDeviceIdAndIdentifier(Long deviceId, String identifier) { + return selectOne(IotDeviceModbusPointDO::getDeviceId, deviceId, + IotDeviceModbusPointDO::getIdentifier, identifier); + } + + default void updateByThingModelId(Long thingModelId, IotDeviceModbusPointDO updateObj) { + update(updateObj, new LambdaQueryWrapperX() + .eq(IotDeviceModbusPointDO::getThingModelId, thingModelId)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java index 2ed27dbb6..8c611d0d4 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java @@ -38,6 +38,10 @@ public interface IotProductMapper extends BaseMapperX { .apply("LOWER(product_key) = {0}", productKey.toLowerCase())); } + default List selectListByStatus(Integer status) { + return selectList(IotProductDO::getStatus, status); + } + default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { return selectCount(new LambdaQueryWrapperX() .geIfPresent(IotProductDO::getCreateTime, createTime)); diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java index f5620c0eb..ac0da6df6 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.job.device; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.framework.iot.config.YudaoIotProperties; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java index a3c4510b2..e2805d53e 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.job.ota; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java index 7e039d032..31c507889 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.mq.consumer.device; import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber { +public class IotDataRuleMessageSubscriber implements IotMessageSubscriber { @Resource private IotDataRuleService dataRuleService; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java similarity index 90% rename from yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java index 19e1f18ba..de74bebcc 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java @@ -9,7 +9,6 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -// TODO @puhui999:后面重构哈 /** * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景 * @@ -17,7 +16,7 @@ import org.springframework.stereotype.Component; */ @Component @Slf4j -public class IotSceneRuleMessageHandler implements IotMessageSubscriber { +public class IotSceneRuleMessageSubscriber implements IotMessageSubscriber { @Resource private IotSceneRuleService sceneRuleService; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java index 77be87fb8..aa9378767 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java @@ -41,7 +41,7 @@ public class IotAlertConfigServiceImpl implements IotAlertConfigService { public Long createAlertConfig(IotAlertConfigSaveReqVO createReqVO) { // 校验关联数据是否存在 sceneRuleService.validateSceneRuleList(createReqVO.getSceneRuleIds()); - adminUserApi.validateUserList(createReqVO.getReceiveUserIds()).checkError(); + adminUserApi.validateUserList(createReqVO.getReceiveUserIds()); // 插入 IotAlertConfigDO alertConfig = BeanUtils.toBean(createReqVO, IotAlertConfigDO.class); @@ -55,7 +55,7 @@ public class IotAlertConfigServiceImpl implements IotAlertConfigService { validateAlertConfigExists(updateReqVO.getId()); // 校验关联数据是否存在 sceneRuleService.validateSceneRuleList(updateReqVO.getSceneRuleIds()); - adminUserApi.validateUserList(updateReqVO.getReceiveUserIds()).checkError(); + adminUserApi.validateUserList(updateReqVO.getReceiveUserIds()); // 更新 IotAlertConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotAlertConfigDO.class); diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java new file mode 100644 index 000000000..2d9ef7ec6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * IoT 设备 Modbus 连接配置 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceModbusConfigService { + + /** + * 保存设备 Modbus 连接配置(新增或更新) + * + * @param saveReqVO 保存信息 + */ + void saveDeviceModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO saveReqVO); + + /** + * 获得设备 Modbus 连接配置 + * + * @param id 编号 + * @return 设备 Modbus 连接配置 + */ + IotDeviceModbusConfigDO getDeviceModbusConfig(Long id); + + /** + * 根据设备编号获得 Modbus 连接配置 + * + * @param deviceId 设备编号 + * @return 设备 Modbus 连接配置 + */ + IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId); + + /** + * 获得 Modbus 连接配置列表 + * + * @param listReqDTO 查询参数 + * @return Modbus 连接配置列表 + */ + List getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java new file mode 100644 index 000000000..8dbe2407b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +/** + * IoT 设备 Modbus 连接配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigService { + + @Resource + private IotDeviceModbusConfigMapper modbusConfigMapper; + + @Resource + private IotDeviceService deviceService; + @Resource + private IotProductService productService; + + @Override + public void saveDeviceModbusConfig(IotDeviceModbusConfigSaveReqVO saveReqVO) { + // 1.1 校验设备存在 + IotDeviceDO device = deviceService.validateDeviceExists(saveReqVO.getDeviceId()); + // 1.2 根据产品 protocolType 条件校验 + IotProductDO product = productService.getProduct(device.getProductId()); + Assert.notNull(product, "产品不存在"); + validateModbusConfigByProtocolType(saveReqVO, product.getProtocolType()); + + // 2. 根据数据库中是否已有配置,决定是新增还是更新 + IotDeviceModbusConfigDO existConfig = modbusConfigMapper.selectByDeviceId(saveReqVO.getDeviceId()); + if (existConfig == null) { + IotDeviceModbusConfigDO modbusConfig = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class); + modbusConfigMapper.insert(modbusConfig); + } else { + IotDeviceModbusConfigDO updateObj = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class, + o -> o.setId(existConfig.getId())); + modbusConfigMapper.updateById(updateObj); + } + } + + @Override + public IotDeviceModbusConfigDO getDeviceModbusConfig(Long id) { + return modbusConfigMapper.selectById(id); + } + + @Override + public IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId) { + return modbusConfigMapper.selectByDeviceId(deviceId); + } + + @Override + public List getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) { + return modbusConfigMapper.selectList(listReqDTO); + } + + private void validateModbusConfigByProtocolType(IotDeviceModbusConfigSaveReqVO saveReqVO, String protocolType) { + IotProtocolTypeEnum protocolTypeEnum = IotProtocolTypeEnum.of(protocolType); + if (protocolTypeEnum == null) { + return; + } + if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_CLIENT) { + Assert.isTrue(StrUtil.isNotEmpty(saveReqVO.getIp()), "Client 模式下,IP 地址不能为空"); + Assert.notNull(saveReqVO.getPort(), "Client 模式下,端口不能为空"); + Assert.notNull(saveReqVO.getTimeout(), "Client 模式下,连接超时时间不能为空"); + Assert.notNull(saveReqVO.getRetryInterval(), "Client 模式下,重试间隔不能为空"); + } else if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_SERVER) { + Assert.notNull(saveReqVO.getMode(), "Server 模式下,工作模式不能为空"); + Assert.notNull(saveReqVO.getFrameFormat(), "Server 模式下,数据帧格式不能为空"); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java new file mode 100644 index 000000000..0be20e105 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * IoT 设备 Modbus 点位配置 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceModbusPointService { + + /** + * 创建设备 Modbus 点位配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO createReqVO); + + /** + * 更新设备 Modbus 点位配置 + * + * @param updateReqVO 更新信息 + */ + void updateDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO updateReqVO); + + /** + * 删除设备 Modbus 点位配置 + * + * @param id 编号 + */ + void deleteDeviceModbusPoint(Long id); + + /** + * 获得设备 Modbus 点位配置 + * + * @param id 编号 + * @return 设备 Modbus 点位配置 + */ + IotDeviceModbusPointDO getDeviceModbusPoint(Long id); + + /** + * 获得设备 Modbus 点位配置分页 + * + * @param pageReqVO 分页查询 + * @return 设备 Modbus 点位配置分页 + */ + PageResult getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO); + + /** + * 物模型变更时,更新关联点位的冗余字段(identifier、name) + * + * @param thingModelId 物模型编号 + * @param identifier 物模型标识符 + * @param name 物模型名称 + */ + void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name); + + /** + * 根据设备编号批量获得启用的点位配置 Map + * + * @param deviceIds 设备编号集合 + * @return 设备点位 Map,key 为设备编号,value 为点位配置列表 + */ + Map> getEnabledDeviceModbusPointMapByDeviceIds(Collection deviceIds); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java new file mode 100644 index 000000000..7683aa7ec --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java @@ -0,0 +1,135 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusPointMapper; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT 设备 Modbus 点位配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointService { + + @Resource + private IotDeviceModbusPointMapper modbusPointMapper; + + @Resource + private IotDeviceService deviceService; + + @Resource + private IotThingModelService thingModelService; + + @Override + public Long createDeviceModbusPoint(IotDeviceModbusPointSaveReqVO createReqVO) { + // 1.1 校验设备存在 + deviceService.validateDeviceExists(createReqVO.getDeviceId()); + // 1.2 校验物模型属性存在 + IotThingModelDO thingModel = validateThingModelExists(createReqVO.getThingModelId()); + // 1.3 校验同一设备下点位唯一性(基于 identifier) + validateDeviceModbusPointUnique(createReqVO.getDeviceId(), thingModel.getIdentifier(), null); + + // 2. 插入 + IotDeviceModbusPointDO modbusPoint = BeanUtils.toBean(createReqVO, IotDeviceModbusPointDO.class, + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName())); + modbusPointMapper.insert(modbusPoint); + return modbusPoint.getId(); + } + + @Override + public void updateDeviceModbusPoint(IotDeviceModbusPointSaveReqVO updateReqVO) { + // 1.1 校验存在 + validateDeviceModbusPointExists(updateReqVO.getId()); + // 1.2 校验设备存在 + deviceService.validateDeviceExists(updateReqVO.getDeviceId()); + // 1.3 校验物模型属性存在 + IotThingModelDO thingModel = validateThingModelExists(updateReqVO.getThingModelId()); + // 1.4 校验同一设备下点位唯一性 + validateDeviceModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId()); + + // 2. 更新 + IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class, + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName())); + modbusPointMapper.updateById(updateObj); + } + + @Override + public void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name) { + IotDeviceModbusPointDO updateObj = new IotDeviceModbusPointDO() + .setIdentifier(identifier).setName(name); + modbusPointMapper.updateByThingModelId(thingModelId, updateObj); + } + + private IotThingModelDO validateThingModelExists(Long id) { + IotThingModelDO thingModel = thingModelService.getThingModel(id); + if (thingModel == null) { + throw exception(THING_MODEL_NOT_EXISTS); + } + return thingModel; + } + + @Override + public void deleteDeviceModbusPoint(Long id) { + // 校验存在 + validateDeviceModbusPointExists(id); + // 删除 + modbusPointMapper.deleteById(id); + } + + private void validateDeviceModbusPointExists(Long id) { + IotDeviceModbusPointDO point = modbusPointMapper.selectById(id); + if (point == null) { + throw exception(DEVICE_MODBUS_POINT_NOT_EXISTS); + } + } + + private void validateDeviceModbusPointUnique(Long deviceId, String identifier, Long excludeId) { + IotDeviceModbusPointDO point = modbusPointMapper.selectByDeviceIdAndIdentifier(deviceId, identifier); + if (point != null && ObjUtil.notEqual(point.getId(), excludeId)) { + throw exception(DEVICE_MODBUS_POINT_EXISTS); + } + } + + @Override + public IotDeviceModbusPointDO getDeviceModbusPoint(Long id) { + return modbusPointMapper.selectById(id); + } + + @Override + public PageResult getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO) { + return modbusPointMapper.selectPage(pageReqVO); + } + + @Override + public Map> getEnabledDeviceModbusPointMapByDeviceIds(Collection deviceIds) { + if (CollUtil.isEmpty(deviceIds)) { + return Collections.emptyMap(); + } + List pointList = modbusPointMapper.selectListByDeviceIdsAndStatus( + deviceIds, CommonStatusEnum.ENABLE.getStatus()); + return convertMultiMap(pointList, IotDeviceModbusPointDO::getDeviceId); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java index 5a622e565..74339af6d 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java index 4ec70e08f..f05776f4c 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java @@ -17,7 +17,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; @@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { if (BooleanUtil.isFalse(product.getRegisterEnabled())) { throw exception(DEVICE_REGISTER_DISABLED); } - // 1.3 验证 productSecret - if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) { + // 1.3 【重要!!!】验证签名 + if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(), + product.getProductSecret(), reqDTO.getSign())) { throw exception(DEVICE_REGISTER_SECRET_INVALID); } return TenantUtils.execute(product.getTenantId(), () -> { diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java index eb75b9154..f9cd77621 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java @@ -3,11 +3,15 @@ package cn.iocoder.yudao.module.iot.service.ota; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaProgressReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaUpgradeReqDTO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; @@ -133,9 +137,9 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { public boolean pushOtaTaskRecord(IotOtaTaskRecordDO record, IotOtaFirmwareDO fireware, IotDeviceDO device) { try { // 1. 推送 OTA 任务记录 - IotDeviceMessage message = IotDeviceMessage.buildOtaUpgrade( - fireware.getVersion(), fireware.getFileUrl(), fireware.getFileSize(), - fireware.getFileDigestAlgorithm(), fireware.getFileDigestValue()); + IotDeviceOtaUpgradeReqDTO params = BeanUtils.toBean(fireware, IotDeviceOtaUpgradeReqDTO.class); + IotDeviceMessage message = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), params); deviceMessageService.sendDeviceMessage(message, device); // 2. 更新 OTA 升级记录状态为进行中 @@ -163,17 +167,16 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { @Override @Transactional(rollbackFor = Exception.class) - @SuppressWarnings("unchecked") public void updateOtaRecordProgress(IotDeviceDO device, IotDeviceMessage message) { // 1.1 参数解析 - Map params = (Map) message.getParams(); - String version = MapUtil.getStr(params, "version"); + IotDeviceOtaProgressReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceOtaProgressReqDTO.class); + String version = params.getVersion(); Assert.notBlank(version, "version 不能为空"); - Integer status = MapUtil.getInt(params, "status"); + Integer status = params.getStatus(); Assert.notNull(status, "status 不能为空"); Assert.notNull(IotOtaTaskRecordStatusEnum.of(status), "status 状态不正确"); - String description = MapUtil.getStr(params, "description"); - Integer progress = MapUtil.getInt(params, "progress"); + String description = params.getDescription(); + Integer progress = params.getProgress(); Assert.notNull(progress, "progress 不能为空"); Assert.isTrue(progress >= 0 && progress <= 100, "progress 必须在 0-100 之间"); // 1.2 查询 OTA 升级记录 diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java index d4292ef52..f31961cfd 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java @@ -10,6 +10,9 @@ import javax.annotation.Nullable; import java.time.LocalDateTime; import java.util.Collection; import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; /** * IoT 产品 Service 接口 @@ -121,6 +124,24 @@ public interface IotProductService { */ Long getProductCount(@Nullable LocalDateTime createTime); + /** + * 批量获得产品列表 + * + * @param ids 产品编号集合 + * @return 产品列表 + */ + List getProductList(Collection ids); + + /** + * 批量获得产品 Map + * + * @param ids 产品编号集合 + * @return 产品 Map(key: 产品编号, value: 产品) + */ + default Map getProductMap(Collection ids) { + return convertMap(getProductList(ids), IotProductDO::getId); + } + /** * 批量校验产品存在 * @@ -128,4 +149,11 @@ public interface IotProductService { */ void validateProductsExist(Collection ids); + /** + * 同步产品的 TDengine 表结构 + * + * 目的:当 MySQL 和 TDengine 不同步时,强制将已发布产品的表结构同步到 TDengine 中 + */ + void syncProductPropertyTable(); + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java index e001f46a2..bf8932b8e 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java @@ -1,9 +1,9 @@ package cn.iocoder.yudao.module.iot.service.product; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; @@ -15,8 +15,10 @@ import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -33,6 +35,7 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; * * @author ahh */ +@Slf4j @Service @Validated public class IotProductServiceImpl implements IotProductService { @@ -40,10 +43,11 @@ public class IotProductServiceImpl implements IotProductService { @Resource private IotProductMapper productMapper; - @Resource - private IotDevicePropertyService devicePropertyDataService; @Resource private IotDeviceService deviceService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private IotDevicePropertyService devicePropertyDataService; @Override public Long createProduct(IotProductSaveReqVO createReqVO) { @@ -171,6 +175,32 @@ public class IotProductServiceImpl implements IotProductService { return productMapper.selectCountByCreateTime(createTime); } + @Override + public List getProductList(Collection ids) { + return productMapper.selectByIds(ids); + } + + @Override + public void syncProductPropertyTable() { + // 1. 获取所有已发布的产品 + List products = productMapper.selectListByStatus( + IotProductStatusEnum.PUBLISHED.getStatus()); + log.info("[syncProductPropertyTable][开始同步,已发布产品数量({})]", products.size()); + + // 2. 遍历同步 TDengine 表结构(创建产品超级表数据模型) + int successCount = 0; + for (IotProductDO product : products) { + try { + devicePropertyDataService.defineDevicePropertyData(product.getId()); + successCount++; + log.info("[syncProductPropertyTable][产品({}/{}) 同步成功]", product.getId(), product.getName()); + } catch (Exception e) { + log.error("[syncProductPropertyTable][产品({}/{}) 同步失败]", product.getId(), product.getName(), e); + } + } + log.info("[syncProductPropertyTable][同步完成,成功({}/{})个]", successCount, products.size()); + } + @Override public void validateProductsExist(Collection ids) { if (CollUtil.isEmpty(ids)) { diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkServiceImpl.java index 09e11c822..5d56c37d3 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkServiceImpl.java @@ -19,9 +19,7 @@ import java.util.Collection; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_SINK_DELETE_FAIL_USED_BY_RULE; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_SINK_NAME_EXISTS; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_SINK_NOT_EXISTS; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; /** * IoT 数据流转目的 Service 实现类 diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java index 431946908..cc282e1b8 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java @@ -14,8 +14,6 @@ import java.time.Duration; // TODO @芋艿:数据库 // TODO @芋艿:mqtt -// TODO @芋艿:tcp -// TODO @芋艿:websocket /** * 可缓存的 {@link IotDataRuleAction} 抽象实现 diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotKafkaDataRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotKafkaDataRuleAction.java index 6d85798bf..94ea1dd49 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotKafkaDataRuleAction.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotKafkaDataRuleAction.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.iot.service.rule.data.action; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkKafkaConfig; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkKafkaConfig; import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.producer.ProducerConfig; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDevicePropertySetSceneRuleAction.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDevicePropertySetSceneRuleAction.java index 79da29844..746e18d92 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDevicePropertySetSceneRuleAction.java @@ -15,7 +15,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.List; -import java.util.Map; /** * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类 @@ -24,7 +23,7 @@ import java.util.Map; */ @Component @Slf4j -public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction { +public class IotDevicePropertySetSceneRuleAction implements IotSceneRuleAction { @Resource private IotDeviceService deviceService; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java index 646990601..d665a14b9 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java @@ -15,9 +15,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** * IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类 diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java index ca04ecd5f..4a8b97475 100644 --- a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -50,6 +51,9 @@ public class IotThingModelServiceImpl implements IotThingModelService { @Resource @Lazy // 延迟加载,解决循环依赖 private IotProductService productService; + @Resource + @Lazy // 延迟加载,解决循环依赖 + private IotDeviceModbusPointService deviceModbusPointService; @Override @Transactional(rollbackFor = Exception.class) @@ -84,7 +88,11 @@ public class IotThingModelServiceImpl implements IotThingModelService { IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); thingModelMapper.updateById(thingModel); - // 3. 删除缓存 + // 3. 同步更新 Modbus 点位的冗余字段(identifier、name) + deviceModbusPointService.updateDeviceModbusPointByThingModel( + updateReqVO.getId(), updateReqVO.getIdentifier(), updateReqVO.getName()); + + // 4. 删除缓存 deleteThingModelListCache(updateReqVO.getProductId()); }