From fe8044c8a1fc5695c2d3fcf0bd58b98052b1d29c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 13 Feb 2026 15:36:17 +0800 Subject: [PATCH] feat(iot):Modbus 支持 Master/Slave 双模式,配置表单和详情按协议类型区分展示 --- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java | 6 ------ yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java | 16 ++++++++-------- yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java | 4 ++-- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java | 12 ++++++------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java | 28 ++++++++++++++-------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java | 2 +- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java | 195 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java | 22 ++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java | 31 +++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java | 317 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java | 6 ++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java | 22 ---------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java | 218 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java | 104 -------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java | 31 ------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java | 60 ------------------------------------------------------------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java | 104 -------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java | 317 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java | 73 ------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java | 6 ------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java | 44 ++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java | 334 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java | 477 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java | 31 +++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java | 280 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java | 6 ++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java | 44 -------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java | 334 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java | 57 --------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java | 477 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java | 210 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java | 149 ----------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java | 31 ------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java | 280 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java | 118 ---------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java | 174 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java | 154 ---------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java | 111 --------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java | 6 ------ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java | 12 +++++++++++- yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml | 16 ++++++++-------- yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java | 114 ------------------------------------------------------------------------------------------------------------------ yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java | 302 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java | 302 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 60 files changed, 4049 insertions(+), 4039 deletions(-) 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 delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.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 delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/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 delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.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 delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.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 delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java index 7eb2729..cfc0841 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java @@ -30,12 +30,6 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { @TableId private Long id; /** - * 产品编号 - * - * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} - */ - private Long productId; - /** * 设备编号 * * 关联 {@link IotDeviceDO#getId()} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java index a97bcb1..2a8ae4e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java @@ -75,14 +75,14 @@ public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigSe if (protocolTypeEnum == null) { return; } - if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_MASTER) { - Assert.isTrue(StrUtil.isNotEmpty(saveReqVO.getIp()), "Master 模式下,IP 地址不能为空"); - Assert.notNull(saveReqVO.getPort(), "Master 模式下,端口不能为空"); - Assert.notNull(saveReqVO.getTimeout(), "Master 模式下,连接超时时间不能为空"); - Assert.notNull(saveReqVO.getRetryInterval(), "Master 模式下,重试间隔不能为空"); - } else if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_SLAVE) { - Assert.notNull(saveReqVO.getMode(), "Slave 模式下,工作模式不能为空"); - Assert.notNull(saveReqVO.getFrameFormat(), "Slave 模式下,数据帧格式不能为空"); + 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-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 index e299364..7536054 100644 --- 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 @@ -25,8 +25,8 @@ public enum IotProtocolTypeEnum implements ArrayValuable { MQTT("mqtt"), EMQX("emqx"), COAP("coap"), - MODBUS_TCP_MASTER("modbus_tcp_master"), - MODBUS_TCP_SLAVE("modbus_tcp_slave"); + 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); 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 8fcc3ea..63894dc 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 @@ -4,8 +4,8 @@ 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.tcpmaster.IotModbusTcpMasterConfig; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveConfig; +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; @@ -169,16 +169,16 @@ public class IotGatewayProperties { private IotEmqxConfig emqx; /** - * Modbus TCP Master 协议配置 + * Modbus TCP Client 协议配置 */ @Valid - private IotModbusTcpMasterConfig modbusTcpMaster; + private IotModbusTcpClientConfig modbusTcpClient; /** - * Modbus TCP Slave 协议配置 + * Modbus TCP Server 协议配置 */ @Valid - private IotModbusTcpSlaveConfig modbusTcpSlave; + private IotModbusTcpServerConfig modbusTcpServer; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java index eb9fd06..310533d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java @@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol; @@ -114,10 +114,10 @@ public class IotProtocolManager implements SmartLifecycle { return createMqttProtocol(config); case EMQX: return createEmqxProtocol(config); - case MODBUS_TCP_MASTER: - return createModbusTcpMasterProtocol(config); - case MODBUS_TCP_SLAVE: - return createModbusTcpSlaveProtocol(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)); @@ -195,23 +195,23 @@ public class IotProtocolManager implements SmartLifecycle { } /** - * 创建 Modbus TCP Master 协议实例 + * 创建 Modbus TCP Client 协议实例 * * @param config 协议实例配置 - * @return Modbus TCP Master 协议实例 + * @return Modbus TCP Client 协议实例 */ - private IotModbusTcpMasterProtocol createModbusTcpMasterProtocol(IotGatewayProperties.ProtocolProperties config) { - return new IotModbusTcpMasterProtocol(config); + private IotModbusTcpClientProtocol createModbusTcpClientProtocol(IotGatewayProperties.ProtocolProperties config) { + return new IotModbusTcpClientProtocol(config); } /** - * 创建 Modbus TCP Slave 协议实例 + * 创建 Modbus TCP Server 协议实例 * * @param config 协议实例配置 - * @return Modbus TCP Slave 协议实例 + * @return Modbus TCP Server 协议实例 */ - private IotModbusTcpSlaveProtocol createModbusTcpSlaveProtocol(IotGatewayProperties.ProtocolProperties config) { - return new IotModbusTcpSlaveProtocol(config); + 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/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 index 6a87334..312e796 100644 --- 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 @@ -9,7 +9,7 @@ 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.tcpslave.codec.IotModbusFrame; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; 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 0000000..1324f3a --- /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/common/utils/IotModbusTcpMasterUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java deleted file mode 100644 index 1f7c14d..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java +++ /dev/null @@ -1,195 +0,0 @@ -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.tcpmaster.manager.IotModbusTcpMasterConnectionManager; -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 IotModbusTcpMasterConnectionManager.ModbusConnection} 执行事务 - * 3. 从响应中提取原始值 - * - * @author 芋道源码 - */ -@UtilityClass -@Slf4j -public class IotModbusTcpMasterUtils { - - /** - * 读取 Modbus 数据 - * - * @param connection Modbus 连接 - * @param slaveId 从站地址 - * @param point 点位配置 - * @return 原始值(int 数组) - */ - public static Future read(IotModbusTcpMasterConnectionManager.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(IotModbusTcpMasterConnectionManager.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 0000000..1743140 --- /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 0000000..f632b62 --- /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 0000000..045e61f --- /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 0000000..6c8a4be --- /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 0000000..2b7e9c2 --- /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 0000000..b28e507 --- /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.modbus.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +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 0000000..bfdacd0 --- /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 0000000..946937d --- /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 0000000..3bd4fa9 --- /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/tcpmaster/IotModbusTcpMasterConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java deleted file mode 100644 index 9b4867c..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * IoT Modbus TCP Master 协议配置 - * - * @author 芋道源码 - */ -@Data -public class IotModbusTcpMasterConfig { - - /** - * 配置刷新间隔(秒) - */ - @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/tcpmaster/IotModbusTcpMasterProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java deleted file mode 100644 index 31271b6..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java +++ /dev/null @@ -1,218 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; - -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.tcpmaster.handler.downstream.IotModbusTcpMasterDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpMasterDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpMasterUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterPollScheduler; -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 Master 协议:主动轮询 Modbus 从站设备数据 - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpMasterProtocol 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 IotModbusTcpMasterConnectionManager connectionManager; - /** - * 下行消息订阅者 - */ - private IotModbusTcpMasterDownstreamSubscriber downstreamSubscriber; - - private final IotModbusTcpMasterConfigCacheService configCacheService; - private final IotModbusTcpMasterPollScheduler pollScheduler; - - public IotModbusTcpMasterProtocol(ProtocolProperties properties) { - IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster(); - Assert.notNull(modbusTcpMasterConfig, "Modbus TCP Master 协议配置(modbusTcpMaster)不能为空"); - 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 IotModbusTcpMasterConfigCacheService(deviceApi); - this.connectionManager = new IotModbusTcpMasterConnectionManager(redissonClient, vertx, - messageService, configCacheService, serverId); - - // 初始化 Handler - IotModbusTcpMasterUpstreamHandler upstreamHandler = new IotModbusTcpMasterUpstreamHandler(messageService, serverId); - - // 初始化轮询调度器 - this.pollScheduler = new IotModbusTcpMasterPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); - } - - @Override - public String getId() { - return properties.getId(); - } - - @Override - public IotProtocolTypeEnum getType() { - return IotProtocolTypeEnum.MODBUS_TCP_MASTER; - } - - @Override - public void start() { - if (running) { - log.warn("[start][IoT Modbus TCP Master 协议 {} 已经在运行中]", getId()); - return; - } - - try { - // 1.1 首次加载配置 - refreshConfig(); - // 1.2 启动配置刷新定时器 - int refreshInterval = properties.getModbusTcpMaster().getConfigRefreshInterval(); - configRefreshTimerId = vertx.setPeriodic( - TimeUnit.SECONDS.toMillis(refreshInterval), - id -> refreshConfig() - ); - running = true; - log.info("[start][IoT Modbus TCP Master 协议 {} 启动成功,serverId={}]", getId(), serverId); - - // 2. 启动下行消息订阅者 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotModbusTcpMasterDownstreamHandler downstreamHandler = new IotModbusTcpMasterDownstreamHandler(connectionManager, - configCacheService); - this.downstreamSubscriber = new IotModbusTcpMasterDownstreamSubscriber(this, downstreamHandler, messageBus); - this.downstreamSubscriber.start(); - } catch (Exception e) { - log.error("[start][IoT Modbus TCP Master 协议 {} 启动失败]", 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 Master 协议 {} 下行消息订阅者已停止]", getId()); - } catch (Exception e) { - log.error("[stop][IoT Modbus TCP Master 协议 {} 下行消息订阅者停止失败]", 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 Master 协议 {} Vertx 已关闭]", getId()); - } catch (Exception e) { - log.error("[stop][IoT Modbus TCP Master 协议 {} Vertx 关闭失败]", getId(), e); - } - } - running = false; - log.info("[stop][IoT Modbus TCP Master 协议 {} 已停止]", 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/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java deleted file mode 100644 index af51e22..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamHandler.java +++ /dev/null @@ -1,104 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.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.IotModbusTcpMasterUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpMasterConnectionManager; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -/** - * IoT Modbus TCP Master 下行消息处理器 - *

- * 负责: - * 1. 处理下行消息(如属性设置 thing.service.property.set) - * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotModbusTcpMasterDownstreamHandler { - - private final IotModbusTcpMasterConnectionManager connectionManager; - private final IotModbusTcpMasterConfigCacheService configCacheService; - - /** - * 处理下行消息 - */ - @SuppressWarnings({"unchecked", "DuplicatedCode"}) - public void handle(IotDeviceMessage message) { - // 1.1 检查是否是属性设置消息 - 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; - } - - // 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 获取连接 - IotModbusTcpMasterConnectionManager.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 写入 - IotModbusTcpMasterUtils.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/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java deleted file mode 100644 index 812e13d..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpMasterDownstreamSubscriber.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.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.tcpmaster.IotModbusTcpMasterProtocol; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpMasterDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { - - private final IotModbusTcpMasterDownstreamHandler downstreamHandler; - - public IotModbusTcpMasterDownstreamSubscriber(IotModbusTcpMasterProtocol protocol, - IotModbusTcpMasterDownstreamHandler 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/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java deleted file mode 100644 index 7fc573d..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpMasterUpstreamHandler.java +++ /dev/null @@ -1,60 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.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 IotModbusTcpMasterUpstreamHandler { - - private final IotDeviceMessageService messageService; - - private final String serverId; - - public IotModbusTcpMasterUpstreamHandler(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/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java deleted file mode 100644 index 46904ad..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConfigCacheService.java +++ /dev/null @@ -1,104 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.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.modbus.IotModbusModeEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -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 Master 配置缓存服务 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotModbusTcpMasterConfigCacheService { - - 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_MASTER.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/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java deleted file mode 100644 index 4fb605a..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterConnectionManager.java +++ /dev/null @@ -1,317 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.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 IotModbusTcpMasterConnectionManager { - - 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 IotModbusTcpMasterConfigCacheService configCacheService; - private final String serverId; - - /** - * 连接池:key = ip:port - */ - private final Map connectionPool = new ConcurrentHashMap<>(); - - /** - * 设备 ID 到连接 key 的映射 - */ - private final Map deviceConnectionMap = new ConcurrentHashMap<>(); - - public IotModbusTcpMasterConnectionManager(RedissonClient redissonClient, Vertx vertx, - IotDeviceMessageService messageService, - IotModbusTcpMasterConfigCacheService 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/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java deleted file mode 100644 index df87b91..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpMasterPollScheduler.java +++ /dev/null @@ -1,73 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.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.IotModbusTcpMasterUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpMasterUpstreamHandler; -import io.vertx.core.Vertx; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT Modbus TCP Master 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpMasterPollScheduler extends AbstractIotModbusPollScheduler { - - private final IotModbusTcpMasterConnectionManager connectionManager; - private final IotModbusTcpMasterUpstreamHandler upstreamHandler; - private final IotModbusTcpMasterConfigCacheService configCacheService; - - public IotModbusTcpMasterPollScheduler(Vertx vertx, - IotModbusTcpMasterConnectionManager connectionManager, - IotModbusTcpMasterUpstreamHandler upstreamHandler, - IotModbusTcpMasterConfigCacheService 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 获取连接 - IotModbusTcpMasterConnectionManager.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 读取 - IotModbusTcpMasterUtils.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/tcpmaster/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java deleted file mode 100644 index 86e3932..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Modbus TCP Master(主站)协议:网关主动连接并轮询 Modbus 从站设备 - *

- * 基于 j2mod 实现,支持 FC01-04 读、FC05/06/15/16 写,定时轮询 + 下发属性设置 - */ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; 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 0000000..5b4fd22 --- /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 0000000..80ce9ee --- /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 0000000..7aeca99 --- /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 0000000..5b92c3e --- /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 0000000..b5c48b2 --- /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 0000000..eb4683f --- /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 0000000..1d36b69 --- /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 0000000..db8a9fd --- /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 0000000..19ba6e9 --- /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.modbus.IotModbusModeEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; +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 0000000..781d8ac --- /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 0000000..9bac7f8 --- /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 0000000..4660f8e --- /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 0000000..c150870 --- /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/modbus/tcpslave/IotModbusTcpSlaveConfig.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java deleted file mode 100644 index 60185f1..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; - -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * IoT Modbus TCP Slave 协议配置 - * - * @author 芋道源码 - */ -@Data -public class IotModbusTcpSlaveConfig { - - /** - * 配置刷新间隔(秒) - */ - @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/tcpslave/IotModbusTcpSlaveProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java deleted file mode 100644 index feb8981..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java +++ /dev/null @@ -1,334 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; - -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.tcpslave.codec.IotModbusFrameDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream.IotModbusTcpSlaveUpstreamHandler; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; -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 Slave 协议 - *

- * 作为 TCP Server 接收设备主动连接: - * 1. 设备通过自定义功能码(FC 65)发送认证请求 - * 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式) - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpSlaveProtocol 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 IotModbusTcpSlaveConnectionManager connectionManager; - /** - * 下行消息订阅者 - */ - private IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; - - private final IotModbusFrameDecoder frameDecoder; - @SuppressWarnings("FieldCanBeLocal") - private final IotModbusFrameEncoder frameEncoder; - - private final IotModbusTcpSlaveConfigCacheService configCacheService; - private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; - private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; - private final IotModbusTcpSlavePollScheduler pollScheduler; - private final IotDeviceMessageService messageService; - - public IotModbusTcpSlaveProtocol(ProtocolProperties properties) { - IotModbusTcpSlaveConfig slaveConfig = properties.getModbusTcpSlave(); - Assert.notNull(slaveConfig, "Modbus TCP Slave 协议配置(modbusTcpSlave)不能为空"); - this.properties = properties; - this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort()); - - // 初始化 Vertx - this.vertx = Vertx.vertx(); - - // 初始化 Manager - IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); - this.connectionManager = new IotModbusTcpSlaveConnectionManager(); - this.configCacheService = new IotModbusTcpSlaveConfigCacheService(deviceApi); - this.pendingRequestManager = new IotModbusTcpSlavePendingRequestManager(); - - // 初始化帧编解码器 - this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode()); - this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode()); - - // 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突) - AtomicInteger transactionIdCounter = new AtomicInteger(0); - // 初始化轮询调度器 - this.pollScheduler = new IotModbusTcpSlavePollScheduler( - vertx, connectionManager, frameEncoder, pendingRequestManager, - slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService); - - // 初始化 Handler - this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); - IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); - this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( - 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_SLAVE; - } - - @Override - public void start() { - if (running) { - log.warn("[start][IoT Modbus TCP Slave 协议 {} 已经在运行中]", getId()); - return; - } - - try { - // 1. 启动配置刷新定时器 - IotModbusTcpSlaveConfig slaveConfig = properties.getModbusTcpSlave(); - 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 Slave 协议 {} 启动成功, serverId={}, port={}]", - getId(), serverId, properties.getPort()); - - // 3. 启动下行消息订阅 - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); - IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( - connectionManager, configCacheService, frameEncoder, this.pollScheduler.getTransactionIdCounter()); - this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( - this, downstreamHandler, messageBus); - downstreamSubscriber.start(); - } catch (Exception e) { - log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", 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 Slave 协议 {} 已停止]", 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/tcpslave/codec/IotModbusFrame.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java deleted file mode 100644 index 635a284..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java +++ /dev/null @@ -1,57 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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/tcpslave/codec/IotModbusFrameDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java deleted file mode 100644 index f333489..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java +++ /dev/null @@ -1,477 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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/tcpslave/codec/IotModbusFrameEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java deleted file mode 100644 index d682c13..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java +++ /dev/null @@ -1,210 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java deleted file mode 100644 index 3867c4b..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java +++ /dev/null @@ -1,149 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * IoT Modbus TCP Slave 下行消息处理器 - *

- * 负责: - * 1. 处理下行消息(如属性设置 thing.service.property.set) - * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备 - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpSlaveDownstreamHandler { - - private final IotModbusTcpSlaveConnectionManager connectionManager; - private final IotModbusTcpSlaveConfigCacheService configCacheService; - private final IotModbusFrameEncoder frameEncoder; - - /** - * TCP 事务 ID 自增器(与 PollScheduler 共享) - */ - private final AtomicInteger transactionIdCounter; - - public IotModbusTcpSlaveDownstreamHandler(IotModbusTcpSlaveConnectionManager connectionManager, - IotModbusTcpSlaveConfigCacheService 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.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/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java deleted file mode 100644 index 2a11cff..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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.tcpslave.IotModbusTcpSlaveProtocol; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpSlaveDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber { - - private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; - - public IotModbusTcpSlaveDownstreamSubscriber(IotModbusTcpSlaveProtocol protocol, - IotModbusTcpSlaveDownstreamHandler 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/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java deleted file mode 100644 index 448e1a9..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java +++ /dev/null @@ -1,280 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; -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 Slave 上行数据处理器 - *

- * 处理: - * 1. 自定义 FC 认证 - * 2. 轮询响应 → 点位翻译 → thing.property.post - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpSlaveUpstreamHandler { - - private static final String METHOD_AUTH = "auth"; - - private final IotDeviceCommonApi deviceApi; - private final IotDeviceMessageService messageService; - private final IotModbusFrameEncoder frameEncoder; - private final IotModbusTcpSlaveConnectionManager connectionManager; - private final IotModbusTcpSlaveConfigCacheService configCacheService; - private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; - private final IotModbusTcpSlavePollScheduler pollScheduler; - private final IotDeviceService deviceService; - - private final String serverId; - - public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, - IotDeviceMessageService messageService, - IotModbusFrameEncoder frameEncoder, - IotModbusTcpSlaveConnectionManager connectionManager, - IotModbusTcpSlaveConfigCacheService configCacheService, - IotModbusTcpSlavePendingRequestManager pendingRequestManager, - IotModbusTcpSlavePollScheduler 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/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java deleted file mode 100644 index e659a89..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java +++ /dev/null @@ -1,118 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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.modbus.IotModbusModeEnum; -import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; -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 Slave 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Slf4j -public class IotModbusTcpSlaveConfigCacheService { - - 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_SLAVE.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_SLAVE.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/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java deleted file mode 100644 index a0a7ad2..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java +++ /dev/null @@ -1,174 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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 Slave 连接管理器 - *

- * 管理设备 TCP 连接:socket ↔ 设备双向映射 - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpSlaveConnectionManager { - - /** - * 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/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java deleted file mode 100644 index cea8f98..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java +++ /dev/null @@ -1,154 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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.tcpslave.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 Slave 待响应请求管理器 - *

- * 管理轮询下发的请求,用于匹配设备响应: - * - TCP 模式:按 transactionId 精确匹配 - * - RTU 模式:按 slaveId + functionCode FIFO 匹配 - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpSlavePendingRequestManager { - - /** - * 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/tcpslave/manager/IotModbusTcpSlavePollScheduler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java deleted file mode 100644 index 31bb8d5..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java +++ /dev/null @@ -1,111 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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.tcpslave.codec.IotModbusFrameEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; -import io.vertx.core.Vertx; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.atomic.AtomicInteger; - -/** - * IoT Modbus TCP Slave 轮询调度器:编码读请求帧,通过 TCP 连接发送到设备,注册 PendingRequest 等待响应 - * - * @author 芋道源码 - */ -@Slf4j -public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollScheduler { - - private final IotModbusTcpSlaveConnectionManager connectionManager; - private final IotModbusFrameEncoder frameEncoder; - private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; - private final IotModbusTcpSlaveConfigCacheService configCacheService; - private final int requestTimeout; - /** - * TCP 事务 ID 自增器(与 DownstreamHandler 共享) - */ - @Getter - private final AtomicInteger transactionIdCounter; - - public IotModbusTcpSlavePollScheduler(Vertx vertx, - IotModbusTcpSlaveConnectionManager connectionManager, - IotModbusFrameEncoder frameEncoder, - IotModbusTcpSlavePendingRequestManager pendingRequestManager, - int requestTimeout, - AtomicInteger transactionIdCounter, - IotModbusTcpSlaveConfigCacheService 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/tcpslave/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java deleted file mode 100644 index cd8f1cb..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Modbus TCP Slave(从站)协议:设备主动连接网关,自定义 FC65 认证后由网关云端轮询 - *

- * TCP Server 模式,支持 MODBUS_TCP / MODBUS_RTU 帧格式自动检测 - */ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; 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 index b3ae6a0..933d56a 100644 --- 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 @@ -1,5 +1,7 @@ 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; @@ -33,9 +35,17 @@ public class IotTcpDownstreamHandler { */ 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. 检查设备连接 + // 1.2 检查设备连接 IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId( message.getDeviceId()); if (connectionInfo == null) { 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 f5c77db..71ee6f3 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 @@ -164,22 +164,22 @@ yudao: trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 # ==================================== - # 针对引入的 Modbus TCP Master 组件的配置 + # 针对引入的 Modbus TCP Client 组件的配置 # ==================================== - - id: modbus-tcp-master-1 + - id: modbus-tcp-client-1 enabled: false - protocol: modbus_tcp_master + protocol: modbus_tcp_client port: 502 - modbus-tcp-master: + modbus-tcp-client: config-refresh-interval: 30 # 配置刷新间隔(秒) # ==================================== - # 针对引入的 Modbus TCP Slave 组件的配置 + # 针对引入的 Modbus TCP Server 组件的配置 # ==================================== - - id: modbus-tcp-slave-1 + - id: modbus-tcp-server-1 enabled: false - protocol: modbus_tcp_slave + protocol: modbus_tcp_server port: 503 - modbus-tcp-slave: + modbus-tcp-server: config-refresh-interval: 30 # 配置刷新间隔(秒) custom-function-code: 65 # 自定义功能码(用于认证等扩展交互) request-timeout: 5000 # Pending Request 超时时间(毫秒) 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 0000000..62724b2 --- /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. + *
+ * + *

可用寄存器: + *

+ * + * @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/tcpmaster/IoTModbusTcpMasterIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java deleted file mode 100644 index 40c8c5c..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IoTModbusTcpMasterIntegrationTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster; - -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 Master 网关连接和读写数据 - * - *

使用步骤: - *

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

可用寄存器: - *

- * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IoTModbusTcpMasterIntegrationTest { - - 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 0000000..24029a1 --- /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 0000000..d00da5f --- /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/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java deleted file mode 100644 index 041c8c3..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveRtuIntegrationTest.java +++ /dev/null @@ -1,302 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; - -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.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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 Slave 协议集成测试 — MODBUS_RTU 帧格式(手动测试) - * - *

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

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 503)
  2. - *
  3. 确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_rtu)
  4. - *
  5. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 自定义功能码认证
    • - *
    • {@link #testPollingResponse()} - 轮询响应
    • - *
    • {@link #testPropertySetWrite()} - 属性设置(接收写指令)
    • - *
    - *
  6. - *
- * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotModbusTcpSlaveRtuIntegrationTest { - - 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_slave_product_demo"; - private static final String DEVICE_NAME = "modbus_tcp_slave_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/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java deleted file mode 100644 index 6360254..0000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveTcpIntegrationTest.java +++ /dev/null @@ -1,302 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; - -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.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.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 Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试) - * - *

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

使用步骤: - *

    - *
  1. 启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-slave 协议,默认端口 503)
  2. - *
  3. 确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_tcp)
  4. - *
  5. 运行以下测试方法: - *
      - *
    • {@link #testAuth()} - 自定义功能码认证
    • - *
    • {@link #testPollingResponse()} - 轮询响应
    • - *
    • {@link #testPropertySetWrite()} - 属性设置(接收写指令)
    • - *
    - *
  6. - *
- * - * @author 芋道源码 - */ -@Slf4j -@Disabled -public class IotModbusTcpSlaveTcpIntegrationTest { - - 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_slave_product_demo"; - private static final String DEVICE_NAME = "modbus_tcp_slave_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(); - } - -} -- libgit2 0.21.4