Commit 346ae3ff48d3ac31b25cad6dbbe03457f90cbfa8
1 parent
a0db8684
feat(iot):modbus-tcp-slave 优化它的 code、decode 逻辑;
Showing
11 changed files
with
955 additions
and
1045 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java
| @@ -4,7 +4,6 @@ import cn.hutool.core.lang.Assert; | @@ -4,7 +4,6 @@ import cn.hutool.core.lang.Assert; | ||
| 4 | import cn.hutool.extra.spring.SpringUtil; | 4 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 5 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 7 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | ||
| 8 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; |
| 9 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; | 8 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 10 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 9 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| @@ -12,9 +11,8 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | @@ -12,9 +11,8 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 12 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; | 11 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; |
| 15 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; | ||
| 16 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; | ||
| 17 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory; | 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; |
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; | ||
| 18 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; | 16 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; |
| 19 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber; | 17 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber; |
| 20 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream.IotModbusTcpSlaveUpstreamHandler; | 18 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream.IotModbusTcpSlaveUpstreamHandler; |
| @@ -23,6 +21,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotM | @@ -23,6 +21,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotM | ||
| 23 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; | 21 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| 24 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; | 22 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; |
| 25 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; | 23 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; |
| 24 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 26 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 25 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 27 | import io.vertx.core.Vertx; | 26 | import io.vertx.core.Vertx; |
| 28 | import io.vertx.core.net.NetServer; | 27 | import io.vertx.core.net.NetServer; |
| @@ -33,19 +32,15 @@ import lombok.Getter; | @@ -33,19 +32,15 @@ import lombok.Getter; | ||
| 33 | import lombok.extern.slf4j.Slf4j; | 32 | import lombok.extern.slf4j.Slf4j; |
| 34 | 33 | ||
| 35 | import java.util.List; | 34 | import java.util.List; |
| 36 | -import java.util.Map; | ||
| 37 | -import java.util.concurrent.ConcurrentHashMap; | ||
| 38 | import java.util.concurrent.TimeUnit; | 35 | import java.util.concurrent.TimeUnit; |
| 39 | 36 | ||
| 40 | -// TODO @AI:不用主动上报! | 37 | +// DONE @AI:不用主动上报! |
| 41 | /** | 38 | /** |
| 42 | * IoT 网关 Modbus TCP Slave 协议 | 39 | * IoT 网关 Modbus TCP Slave 协议 |
| 43 | * <p> | 40 | * <p> |
| 44 | * 作为 TCP Server 接收设备主动连接: | 41 | * 作为 TCP Server 接收设备主动连接: |
| 45 | * 1. 设备通过自定义功能码(FC 65)发送认证请求 | 42 | * 1. 设备通过自定义功能码(FC 65)发送认证请求 |
| 46 | - * 2. 认证成功后,根据设备配置的 mode 决定工作模式: | ||
| 47 | - * - mode=1(云端轮询):网关主动发送 Modbus 读请求,设备响应 | ||
| 48 | - * - mode=2(主动上报):设备主动上报数据,网关透传 | 43 | + * 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式) |
| 49 | * | 44 | * |
| 50 | * @author 芋道源码 | 45 | * @author 芋道源码 |
| 51 | */ | 46 | */ |
| @@ -85,20 +80,16 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | @@ -85,20 +80,16 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | ||
| 85 | */ | 80 | */ |
| 86 | private Long requestCleanupTimerId; | 81 | private Long requestCleanupTimerId; |
| 87 | 82 | ||
| 88 | - /** | ||
| 89 | - * 未认证连接的帧格式缓存:socket → 检测到的帧格式 | ||
| 90 | - */ | ||
| 91 | - private final Map<NetSocket, IotModbusFrameFormatEnum> pendingFrameFormats = new ConcurrentHashMap<>(); | ||
| 92 | - | ||
| 93 | // ========== 各组件 ========== | 83 | // ========== 各组件 ========== |
| 84 | + // TODO @芋艿:稍后排序下,有点小乱; | ||
| 94 | 85 | ||
| 95 | private final IotModbusTcpSlaveConfig slaveConfig; | 86 | private final IotModbusTcpSlaveConfig slaveConfig; |
| 96 | - private final IotModbusFrameCodec frameCodec; | 87 | + private final IotModbusFrameDecoder frameDecoder; |
| 88 | + private final IotModbusFrameEncoder frameEncoder; | ||
| 97 | private final IotModbusTcpSlaveConnectionManager connectionManager; | 89 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 98 | private final IotModbusTcpSlaveConfigCacheService configCacheService; | 90 | private final IotModbusTcpSlaveConfigCacheService configCacheService; |
| 99 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; | 91 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; |
| 100 | private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; | 92 | private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; |
| 101 | - private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; | ||
| 102 | private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; | 93 | private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; |
| 103 | private final IotModbusTcpSlavePollScheduler pollScheduler; | 94 | private final IotModbusTcpSlavePollScheduler pollScheduler; |
| 104 | 95 | ||
| @@ -118,33 +109,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | @@ -118,33 +109,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | ||
| 118 | this.pendingRequestManager = new IotModbusTcpSlavePendingRequestManager(); | 109 | this.pendingRequestManager = new IotModbusTcpSlavePendingRequestManager(); |
| 119 | 110 | ||
| 120 | // 初始化帧编解码器 | 111 | // 初始化帧编解码器 |
| 121 | - this.frameCodec = new IotModbusFrameCodec(slaveConfig.getCustomFunctionCode()); | ||
| 122 | - | ||
| 123 | - // 初始化 Handler | ||
| 124 | - IotModbusDataConverter dataConverter = new IotModbusDataConverter(); | ||
| 125 | - IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); | ||
| 126 | - this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( | ||
| 127 | - deviceApi, messageService, dataConverter, frameCodec, | ||
| 128 | - connectionManager, configCacheService, pendingRequestManager, serverId); | ||
| 129 | - this.downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( | ||
| 130 | - connectionManager, configCacheService, dataConverter, frameCodec); | 112 | + this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode()); |
| 113 | + this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode()); | ||
| 131 | 114 | ||
| 132 | // 初始化轮询调度器 | 115 | // 初始化轮询调度器 |
| 133 | this.pollScheduler = new IotModbusTcpSlavePollScheduler( | 116 | this.pollScheduler = new IotModbusTcpSlavePollScheduler( |
| 134 | - vertx, connectionManager, frameCodec, pendingRequestManager, | 117 | + vertx, connectionManager, frameEncoder, pendingRequestManager, |
| 135 | slaveConfig.getRequestTimeout()); | 118 | slaveConfig.getRequestTimeout()); |
| 136 | 119 | ||
| 137 | - // 设置认证成功回调:启动轮询 | ||
| 138 | - // TODO @AI:感觉直接去调用,不用注册回调了(更简洁) | ||
| 139 | - this.upstreamHandler.setOnAuthSuccess((deviceId, config) -> { | ||
| 140 | - if (config.getMode() != null | ||
| 141 | - && config.getMode().equals(IotModbusModeEnum.POLLING.getMode())) { | ||
| 142 | - pollScheduler.updatePolling(config); | ||
| 143 | - } | ||
| 144 | - }); | 120 | + // 初始化 Handler |
| 121 | + IotModbusDataConverter dataConverter = new IotModbusDataConverter(); | ||
| 122 | + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); | ||
| 123 | + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); | ||
| 124 | + this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( | ||
| 125 | + deviceApi, messageService, dataConverter, frameEncoder, | ||
| 126 | + connectionManager, configCacheService, pendingRequestManager, | ||
| 127 | + pollScheduler, deviceService, serverId); | ||
| 145 | 128 | ||
| 146 | // 初始化下行消息订阅者 | 129 | // 初始化下行消息订阅者 |
| 147 | IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); | 130 | IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); |
| 131 | + IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( | ||
| 132 | + connectionManager, configCacheService, dataConverter, frameEncoder); | ||
| 148 | this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( | 133 | this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( |
| 149 | this, downstreamHandler, messageBus); | 134 | this, downstreamHandler, messageBus); |
| 150 | } | 135 | } |
| @@ -168,6 +153,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | @@ -168,6 +153,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | ||
| 168 | 153 | ||
| 169 | try { | 154 | try { |
| 170 | // 1.1 首次加载配置 | 155 | // 1.1 首次加载配置 |
| 156 | + // TODO @AI:可能首次不用加载;你在想想; | ||
| 171 | refreshConfig(); | 157 | refreshConfig(); |
| 172 | // 1.2 启动配置刷新定时器 | 158 | // 1.2 启动配置刷新定时器 |
| 173 | int refreshInterval = slaveConfig.getConfigRefreshInterval(); | 159 | int refreshInterval = slaveConfig.getConfigRefreshInterval(); |
| @@ -190,10 +176,21 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | @@ -190,10 +176,21 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | ||
| 190 | downstreamSubscriber.start(); | 176 | downstreamSubscriber.start(); |
| 191 | } catch (Exception e) { | 177 | } catch (Exception e) { |
| 192 | log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e); | 178 | log.error("[start][IoT Modbus TCP Slave 协议 {} 启动失败]", getId(), e); |
| 179 | + if (configRefreshTimerId != null) { | ||
| 180 | + vertx.cancelTimer(configRefreshTimerId); | ||
| 181 | + configRefreshTimerId = null; | ||
| 182 | + } | ||
| 183 | + if (requestCleanupTimerId != null) { | ||
| 184 | + vertx.cancelTimer(requestCleanupTimerId); | ||
| 185 | + requestCleanupTimerId = null; | ||
| 186 | + } | ||
| 187 | + connectionManager.closeAll(); | ||
| 188 | + if (netServer != null) { | ||
| 189 | + netServer.close(); | ||
| 190 | + } | ||
| 193 | if (vertx != null) { | 191 | if (vertx != null) { |
| 194 | vertx.close(); | 192 | vertx.close(); |
| 195 | } | 193 | } |
| 196 | - // TODO @AI:其它相关的 close; | ||
| 197 | throw e; | 194 | throw e; |
| 198 | } | 195 | } |
| 199 | } | 196 | } |
| @@ -252,20 +249,19 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | @@ -252,20 +249,19 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | ||
| 252 | * 启动 TCP Server | 249 | * 启动 TCP Server |
| 253 | */ | 250 | */ |
| 254 | private void startTcpServer() { | 251 | private void startTcpServer() { |
| 255 | - // TODO @AI:host 一定要设置么? | ||
| 256 | // 1. 创建 TCP Server | 252 | // 1. 创建 TCP Server |
| 257 | NetServerOptions options = new NetServerOptions() | 253 | NetServerOptions options = new NetServerOptions() |
| 258 | - .setPort(properties.getPort()) | ||
| 259 | - .setHost("0.0.0.0"); | 254 | + .setPort(properties.getPort()); |
| 260 | netServer = vertx.createNetServer(options); | 255 | netServer = vertx.createNetServer(options); |
| 261 | 256 | ||
| 262 | // 2. 设置连接处理器 | 257 | // 2. 设置连接处理器 |
| 263 | netServer.connectHandler(this::handleConnection); | 258 | netServer.connectHandler(this::handleConnection); |
| 264 | - // TODO @AI:是不是 sync 就好,不用 onSuccess/onFailure 了?感觉更简洁。失败,肯定就要抛出异常,结束初始化了! | ||
| 265 | - netServer.listen() | ||
| 266 | - .onSuccess(server -> log.info("[startTcpServer][TCP Server 启动成功, port={}]", | ||
| 267 | - server.actualPort())) | ||
| 268 | - .onFailure(e -> log.error("[startTcpServer][TCP Server 启动失败]", e)); | 259 | + try { |
| 260 | + netServer.listen().toCompletionStage().toCompletableFuture().get(); | ||
| 261 | + log.info("[startTcpServer][TCP Server 启动成功, port={}]", properties.getPort()); | ||
| 262 | + } catch (Exception e) { | ||
| 263 | + throw new RuntimeException("[startTcpServer][TCP Server 启动失败]", e); | ||
| 264 | + } | ||
| 269 | } | 265 | } |
| 270 | 266 | ||
| 271 | /** | 267 | /** |
| @@ -274,58 +270,24 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | @@ -274,58 +270,24 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { | ||
| 274 | private void handleConnection(NetSocket socket) { | 270 | private void handleConnection(NetSocket socket) { |
| 275 | log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress()); | 271 | log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress()); |
| 276 | 272 | ||
| 277 | - // 1.1 创建带帧格式检测的 RecordParser | ||
| 278 | - // TODO @AI:看看怎么从这个类里面,拿出去;让这个类的职责更单一; | ||
| 279 | - RecordParser parser = IotModbusRecordParserFactory.create( | ||
| 280 | - slaveConfig.getCustomFunctionCode(), | ||
| 281 | - // 完整帧回调 | ||
| 282 | - // TODO @AI:感觉搞个独立的类,稍微好点?! | ||
| 283 | - frameBuffer -> { | ||
| 284 | - byte[] frameBytes = frameBuffer.getBytes(); | ||
| 285 | - // 获取该连接的帧格式 | ||
| 286 | - ConnectionInfo connInfo = connectionManager.getConnectionInfo(socket); | ||
| 287 | - IotModbusFrameFormatEnum frameFormat = connInfo != null ? connInfo.getFrameFormat() : null; | ||
| 288 | - if (frameFormat == null) { | ||
| 289 | - // 未认证的连接,使用首帧检测到的帧格式 | ||
| 290 | - frameFormat = pendingFrameFormats.get(socket); | ||
| 291 | - } | ||
| 292 | - if (frameFormat == null) { | ||
| 293 | - log.warn("[handleConnection][帧格式未检测到, remoteAddress={}]", socket.remoteAddress()); | ||
| 294 | - return; | ||
| 295 | - } | ||
| 296 | - | ||
| 297 | - // 解码帧 | ||
| 298 | - IotModbusFrame frame = frameCodec.decodeResponse(frameBytes, frameFormat); | ||
| 299 | - // 交给 UpstreamHandler 处理 | ||
| 300 | - upstreamHandler.handleFrame(socket, frame, frameFormat); | ||
| 301 | - }, | ||
| 302 | - // 帧格式检测回调:保存到未认证缓存 | ||
| 303 | - detectedFormat -> { | ||
| 304 | - // TODO @AI:是不是不用缓存,每次都探测;因为一般 auth 首包后,基本也没探测的诉求了! | ||
| 305 | - pendingFrameFormats.put(socket, detectedFormat); | ||
| 306 | - // 如果连接已注册(不太可能在检测阶段),也更新 | ||
| 307 | - // TODO @AI:是否非必须?! | ||
| 308 | - connectionManager.setFrameFormat(socket, detectedFormat); | ||
| 309 | - log.debug("[handleConnection][帧格式检测: {}, remoteAddress={}]", | ||
| 310 | - detectedFormat, socket.remoteAddress()); | ||
| 311 | - } | ||
| 312 | - ); | ||
| 313 | - // 1.2 设置数据处理器 | ||
| 314 | - socket.handler(parser); | 273 | + // 1. 创建 RecordParser 并设置为数据处理器 |
| 274 | + RecordParser recordParser = frameDecoder.createRecordParser((frame, frameFormat) -> { | ||
| 275 | + // 【重要】帧处理分发,即消息处理 | ||
| 276 | + upstreamHandler.handleFrame(socket, frame, frameFormat); | ||
| 277 | + }); | ||
| 278 | + socket.handler(recordParser); | ||
| 315 | 279 | ||
| 316 | // 2.1 连接关闭处理 | 280 | // 2.1 连接关闭处理 |
| 317 | socket.closeHandler(v -> { | 281 | socket.closeHandler(v -> { |
| 318 | - pendingFrameFormats.remove(socket); | ||
| 319 | ConnectionInfo info = connectionManager.removeConnection(socket); | 282 | ConnectionInfo info = connectionManager.removeConnection(socket); |
| 320 | - // TODO @AI:if return 简化下; | ||
| 321 | - if (info != null && info.getDeviceId() != null) { | ||
| 322 | - pollScheduler.stopPolling(info.getDeviceId()); | ||
| 323 | - pendingRequestManager.removeDevice(info.getDeviceId()); | ||
| 324 | - log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", | ||
| 325 | - info.getDeviceId(), socket.remoteAddress()); | ||
| 326 | - } else { | 283 | + if (info == null || info.getDeviceId() == null) { |
| 327 | log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress()); | 284 | log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress()); |
| 285 | + return; | ||
| 328 | } | 286 | } |
| 287 | + pollScheduler.stopPolling(info.getDeviceId()); | ||
| 288 | + pendingRequestManager.removeDevice(info.getDeviceId()); | ||
| 289 | + log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", | ||
| 290 | + info.getDeviceId(), socket.remoteAddress()); | ||
| 329 | }); | 291 | }); |
| 330 | // 2.2 异常处理 | 292 | // 2.2 异常处理 |
| 331 | socket.exceptionHandler(e -> { | 293 | socket.exceptionHandler(e -> { |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameCodec.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | ||
| 4 | -import lombok.extern.slf4j.Slf4j; | ||
| 5 | - | ||
| 6 | -import java.nio.ByteBuffer; | ||
| 7 | -import java.nio.ByteOrder; | ||
| 8 | -import java.nio.charset.StandardCharsets; | ||
| 9 | - | ||
| 10 | -/** | ||
| 11 | - * IoT Modbus 帧编解码器 | ||
| 12 | - * <p> | ||
| 13 | - * 纯 Modbus 协议编解码,不处理 TCP 粘包(由 RecordParser 处理)。 | ||
| 14 | - * 支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式,以及自定义功能码扩展。 | ||
| 15 | - * | ||
| 16 | - * @author 芋道源码 | ||
| 17 | - */ | ||
| 18 | -@Slf4j | ||
| 19 | -public class IotModbusFrameCodec { | ||
| 20 | - | ||
| 21 | - private final int customFunctionCode; | ||
| 22 | - | ||
| 23 | - public IotModbusFrameCodec(int customFunctionCode) { | ||
| 24 | - this.customFunctionCode = customFunctionCode; | ||
| 25 | - } | ||
| 26 | - | ||
| 27 | - // ==================== 解码 ==================== | ||
| 28 | - | ||
| 29 | - /** | ||
| 30 | - * 解码响应帧(拆包后的完整帧 byte[]) | ||
| 31 | - * | ||
| 32 | - * @param data 完整帧字节数组 | ||
| 33 | - * @param format 帧格式 | ||
| 34 | - * @return 解码后的 IotModbusFrame | ||
| 35 | - */ | ||
| 36 | - public IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) { | ||
| 37 | - if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { | ||
| 38 | - return decodeTcpResponse(data); | ||
| 39 | - } else { | ||
| 40 | - return decodeRtuResponse(data); | ||
| 41 | - } | ||
| 42 | - } | ||
| 43 | - | ||
| 44 | - /** | ||
| 45 | - * 解码 MODBUS_TCP 响应 | ||
| 46 | - * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...] | ||
| 47 | - */ | ||
| 48 | - private IotModbusFrame decodeTcpResponse(byte[] data) { | ||
| 49 | - if (data.length < 8) { | ||
| 50 | - log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length); | ||
| 51 | - return null; | ||
| 52 | - } | ||
| 53 | - ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); | ||
| 54 | - int transactionId = buf.getShort() & 0xFFFF; | ||
| 55 | - buf.getShort(); // protocolId(跳过)// TODO @AI:跳过原因,最好写下; | ||
| 56 | - buf.getShort(); // length(跳过)// TODO @AI:跳过原因,最好写下; | ||
| 57 | - int slaveId = buf.get() & 0xFF; | ||
| 58 | - int functionCode = buf.get() & 0xFF; | ||
| 59 | - // 提取 PDU 数据(从 functionCode 之后到末尾) | ||
| 60 | - byte[] pdu = new byte[data.length - 8]; | ||
| 61 | - System.arraycopy(data, 8, pdu, 0, pdu.length); | ||
| 62 | - | ||
| 63 | - // 构建 IotModbusFrame | ||
| 64 | - return buildFrame(slaveId, functionCode, pdu, transactionId); | ||
| 65 | - } | ||
| 66 | - | ||
| 67 | - /** | ||
| 68 | - * 解码 MODBUS_RTU 响应 | ||
| 69 | - * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)] | ||
| 70 | - */ | ||
| 71 | - private IotModbusFrame decodeRtuResponse(byte[] data) { | ||
| 72 | - if (data.length < 4) { | ||
| 73 | - log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length); | ||
| 74 | - return null; | ||
| 75 | - } | ||
| 76 | - // 校验 CRC | ||
| 77 | - if (!verifyCrc16(data)) { | ||
| 78 | - log.warn("[decodeRtuResponse][CRC 校验失败]"); | ||
| 79 | - return null; | ||
| 80 | - } | ||
| 81 | - int slaveId = data[0] & 0xFF; | ||
| 82 | - int functionCode = data[1] & 0xFF; | ||
| 83 | - // PDU 数据(不含 slaveId、functionCode、CRC) | ||
| 84 | - byte[] pdu = new byte[data.length - 4]; | ||
| 85 | - System.arraycopy(data, 2, pdu, 0, pdu.length); | ||
| 86 | - | ||
| 87 | - // 构建 IotModbusFrame | ||
| 88 | - return buildFrame(slaveId, functionCode, pdu, null); | ||
| 89 | - } | ||
| 90 | - | ||
| 91 | - /** | ||
| 92 | - * 构建 IotModbusFrame | ||
| 93 | - */ | ||
| 94 | - private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) { | ||
| 95 | - IotModbusFrame frame = new IotModbusFrame() | ||
| 96 | - .setSlaveId(slaveId) | ||
| 97 | - .setFunctionCode(functionCode) | ||
| 98 | - .setPdu(pdu) | ||
| 99 | - .setTransactionId(transactionId); | ||
| 100 | - | ||
| 101 | - // 异常响应 | ||
| 102 | - // TODO @AI:0x80 看看是不是要枚举; | ||
| 103 | - if ((functionCode & 0x80) != 0) { | ||
| 104 | - frame.setException(true); | ||
| 105 | - // TODO @AI:0x7f 看看是不是要枚举; | ||
| 106 | - frame.setFunctionCode(functionCode & 0x7F); | ||
| 107 | - if (pdu.length >= 1) { | ||
| 108 | - frame.setExceptionCode(pdu[0] & 0xFF); | ||
| 109 | - } | ||
| 110 | - return frame; | ||
| 111 | - } | ||
| 112 | - | ||
| 113 | - // 自定义功能码 | ||
| 114 | - if (functionCode == customFunctionCode) { | ||
| 115 | - // data 区格式:[byteCount(1)] [JSON data(N)] | ||
| 116 | - if (pdu.length >= 1) { | ||
| 117 | - int byteCount = pdu[0] & 0xFF; | ||
| 118 | - if (pdu.length >= 1 + byteCount) { | ||
| 119 | - frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8)); | ||
| 120 | - } | ||
| 121 | - } | ||
| 122 | - } | ||
| 123 | - return frame; | ||
| 124 | - } | ||
| 125 | - | ||
| 126 | - // ==================== 编码 ==================== | ||
| 127 | - | ||
| 128 | - /** | ||
| 129 | - * 编码读请求 | ||
| 130 | - * | ||
| 131 | - * @param slaveId 从站地址 | ||
| 132 | - * @param functionCode 功能码 | ||
| 133 | - * @param startAddress 起始寄存器地址 | ||
| 134 | - * @param quantity 寄存器数量 | ||
| 135 | - * @param format 帧格式 | ||
| 136 | - * @param transactionId 事务 ID(TCP 模式下使用) | ||
| 137 | - * @return 编码后的字节数组 | ||
| 138 | - */ | ||
| 139 | - public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity, | ||
| 140 | - IotModbusFrameFormatEnum format, int transactionId) { | ||
| 141 | - // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)] | ||
| 142 | - byte[] pdu = new byte[5]; | ||
| 143 | - pdu[0] = (byte) functionCode; | ||
| 144 | - pdu[1] = (byte) ((startAddress >> 8) & 0xFF); | ||
| 145 | - pdu[2] = (byte) (startAddress & 0xFF); | ||
| 146 | - pdu[3] = (byte) ((quantity >> 8) & 0xFF); | ||
| 147 | - pdu[4] = (byte) (quantity & 0xFF); | ||
| 148 | - return wrapFrame(slaveId, pdu, format, transactionId); | ||
| 149 | - } | ||
| 150 | - | ||
| 151 | - /** | ||
| 152 | - * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) | ||
| 153 | - * | ||
| 154 | - * @param slaveId 从站地址 | ||
| 155 | - * @param functionCode 功能码 | ||
| 156 | - * @param address 寄存器地址 | ||
| 157 | - * @param value 值 | ||
| 158 | - * @param format 帧格式 | ||
| 159 | - * @param transactionId 事务 ID | ||
| 160 | - * @return 编码后的字节数组 | ||
| 161 | - */ | ||
| 162 | - public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value, | ||
| 163 | - IotModbusFrameFormatEnum format, int transactionId) { | ||
| 164 | - // PDU: [FC(1)] [Address(2)] [Value(2)] | ||
| 165 | - byte[] pdu = new byte[5]; | ||
| 166 | - pdu[0] = (byte) functionCode; | ||
| 167 | - pdu[1] = (byte) ((address >> 8) & 0xFF); | ||
| 168 | - pdu[2] = (byte) (address & 0xFF); | ||
| 169 | - pdu[3] = (byte) ((value >> 8) & 0xFF); | ||
| 170 | - pdu[4] = (byte) (value & 0xFF); | ||
| 171 | - return wrapFrame(slaveId, pdu, format, transactionId); | ||
| 172 | - } | ||
| 173 | - | ||
| 174 | - /** | ||
| 175 | - * 编码写多个寄存器请求(FC16) | ||
| 176 | - * | ||
| 177 | - * @param slaveId 从站地址 | ||
| 178 | - * @param address 起始地址 | ||
| 179 | - * @param values 值数组 | ||
| 180 | - * @param format 帧格式 | ||
| 181 | - * @param transactionId 事务 ID | ||
| 182 | - * @return 编码后的字节数组 | ||
| 183 | - */ | ||
| 184 | - public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values, | ||
| 185 | - IotModbusFrameFormatEnum format, int transactionId) { | ||
| 186 | - // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)] | ||
| 187 | - int quantity = values.length; | ||
| 188 | - int byteCount = quantity * 2; | ||
| 189 | - byte[] pdu = new byte[6 + byteCount]; | ||
| 190 | - pdu[0] = (byte) 16; // FC16 | ||
| 191 | - pdu[1] = (byte) ((address >> 8) & 0xFF); | ||
| 192 | - pdu[2] = (byte) (address & 0xFF); | ||
| 193 | - pdu[3] = (byte) ((quantity >> 8) & 0xFF); | ||
| 194 | - pdu[4] = (byte) (quantity & 0xFF); | ||
| 195 | - pdu[5] = (byte) byteCount; | ||
| 196 | - for (int i = 0; i < quantity; i++) { | ||
| 197 | - pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF); | ||
| 198 | - pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF); | ||
| 199 | - } | ||
| 200 | - return wrapFrame(slaveId, pdu, format, transactionId); | ||
| 201 | - } | ||
| 202 | - | ||
| 203 | - /** | ||
| 204 | - * 编码自定义功能码帧(认证响应等) | ||
| 205 | - * | ||
| 206 | - * @param slaveId 从站地址 | ||
| 207 | - * @param jsonData JSON 数据 | ||
| 208 | - * @param format 帧格式 | ||
| 209 | - * @param transactionId 事务 ID | ||
| 210 | - * @return 编码后的字节数组 | ||
| 211 | - */ | ||
| 212 | - public byte[] encodeCustomFrame(int slaveId, String jsonData, | ||
| 213 | - IotModbusFrameFormatEnum format, int transactionId) { | ||
| 214 | - byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8); | ||
| 215 | - // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)] | ||
| 216 | - byte[] pdu = new byte[2 + jsonBytes.length]; | ||
| 217 | - pdu[0] = (byte) customFunctionCode; | ||
| 218 | - pdu[1] = (byte) jsonBytes.length; | ||
| 219 | - System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length); | ||
| 220 | - return wrapFrame(slaveId, pdu, format, transactionId); | ||
| 221 | - } | ||
| 222 | - | ||
| 223 | - // ==================== 帧封装 ==================== | ||
| 224 | - | ||
| 225 | - /** | ||
| 226 | - * 将 PDU 封装为完整帧 | ||
| 227 | - * | ||
| 228 | - * @param slaveId 从站地址 | ||
| 229 | - * @param pdu PDU 数据(含 functionCode) | ||
| 230 | - * @param format 帧格式 | ||
| 231 | - * @param transactionId 事务 ID(TCP 模式下使用) | ||
| 232 | - * @return 完整帧字节数组 | ||
| 233 | - */ | ||
| 234 | - private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, int transactionId) { | ||
| 235 | - if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { | ||
| 236 | - return wrapTcpFrame(slaveId, pdu, transactionId); | ||
| 237 | - } else { | ||
| 238 | - return wrapRtuFrame(slaveId, pdu); | ||
| 239 | - } | ||
| 240 | - } | ||
| 241 | - | ||
| 242 | - /** | ||
| 243 | - * 封装 MODBUS_TCP 帧 | ||
| 244 | - * [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...] | ||
| 245 | - */ | ||
| 246 | - private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) { | ||
| 247 | - int length = 1 + pdu.length; // UnitId + PDU | ||
| 248 | - byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU | ||
| 249 | - // MBAP Header | ||
| 250 | - frame[0] = (byte) ((transactionId >> 8) & 0xFF); | ||
| 251 | - frame[1] = (byte) (transactionId & 0xFF); | ||
| 252 | - frame[2] = 0; // Protocol ID high | ||
| 253 | - frame[3] = 0; // Protocol ID low | ||
| 254 | - frame[4] = (byte) ((length >> 8) & 0xFF); | ||
| 255 | - frame[5] = (byte) (length & 0xFF); | ||
| 256 | - // Unit ID | ||
| 257 | - frame[6] = (byte) slaveId; | ||
| 258 | - // PDU | ||
| 259 | - System.arraycopy(pdu, 0, frame, 7, pdu.length); | ||
| 260 | - return frame; | ||
| 261 | - } | ||
| 262 | - | ||
| 263 | - /** | ||
| 264 | - * 封装 MODBUS_RTU 帧 | ||
| 265 | - * [SlaveId(1)] [PDU...] [CRC(2)] | ||
| 266 | - */ | ||
| 267 | - private byte[] wrapRtuFrame(int slaveId, byte[] pdu) { | ||
| 268 | - byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC | ||
| 269 | - frame[0] = (byte) slaveId; | ||
| 270 | - System.arraycopy(pdu, 0, frame, 1, pdu.length); | ||
| 271 | - // 计算并追加 CRC16 | ||
| 272 | - int crc = calculateCrc16(frame, frame.length - 2); | ||
| 273 | - frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low | ||
| 274 | - frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High | ||
| 275 | - return frame; | ||
| 276 | - } | ||
| 277 | - | ||
| 278 | - // ==================== CRC16 工具 ==================== | ||
| 279 | - | ||
| 280 | - // TODO @AI:hutool 等,有没工具类可以用 | ||
| 281 | - /** | ||
| 282 | - * 计算 CRC-16/MODBUS | ||
| 283 | - * | ||
| 284 | - * @param data 数据 | ||
| 285 | - * @param length 计算长度 | ||
| 286 | - * @return CRC16 值 | ||
| 287 | - */ | ||
| 288 | - public static int calculateCrc16(byte[] data, int length) { | ||
| 289 | - int crc = 0xFFFF; | ||
| 290 | - for (int i = 0; i < length; i++) { | ||
| 291 | - crc ^= (data[i] & 0xFF); | ||
| 292 | - for (int j = 0; j < 8; j++) { | ||
| 293 | - if ((crc & 0x0001) != 0) { | ||
| 294 | - crc >>= 1; | ||
| 295 | - crc ^= 0xA001; | ||
| 296 | - } else { | ||
| 297 | - crc >>= 1; | ||
| 298 | - } | ||
| 299 | - } | ||
| 300 | - } | ||
| 301 | - return crc; | ||
| 302 | - } | ||
| 303 | - | ||
| 304 | - // TODO @AI:hutool 等,有没工具类可以用 | ||
| 305 | - /** | ||
| 306 | - * 校验 CRC16 | ||
| 307 | - */ | ||
| 308 | - private boolean verifyCrc16(byte[] data) { | ||
| 309 | - if (data.length < 3) { | ||
| 310 | - return false; | ||
| 311 | - } | ||
| 312 | - int computed = calculateCrc16(data, data.length - 2); | ||
| 313 | - int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); | ||
| 314 | - return computed == received; | ||
| 315 | - } | ||
| 316 | - | ||
| 317 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | ||
| 4 | +import io.vertx.core.Handler; | ||
| 5 | +import io.vertx.core.buffer.Buffer; | ||
| 6 | +import io.vertx.core.parsetools.RecordParser; | ||
| 7 | +import lombok.RequiredArgsConstructor; | ||
| 8 | +import lombok.extern.slf4j.Slf4j; | ||
| 9 | + | ||
| 10 | +import java.nio.ByteBuffer; | ||
| 11 | +import java.nio.ByteOrder; | ||
| 12 | +import java.nio.charset.StandardCharsets; | ||
| 13 | +import java.util.function.BiConsumer; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * IoT Modbus 帧解码器:集成 TCP 拆包 + 帧格式探测 + 帧解码,一条龙完成从 TCP 字节流到 IotModbusFrame 的转换。 | ||
| 17 | + * <p> | ||
| 18 | + * 流程: | ||
| 19 | + * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU | ||
| 20 | + * 2. 检测后切换到对应的拆包 Handler,并将首包 6 字节通过 handleFirstBytes() 交给新 Handler 处理 | ||
| 21 | + * 3. 拆包完成后解码为 IotModbusFrame,通过回调返回 | ||
| 22 | + * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动) | ||
| 23 | + * - MODBUS_RTU:功能码驱动的状态机 | ||
| 24 | + * | ||
| 25 | + * @author 芋道源码 | ||
| 26 | + */ | ||
| 27 | +@Slf4j | ||
| 28 | +public class IotModbusFrameDecoder { | ||
| 29 | + | ||
| 30 | + private final int customFunctionCode; | ||
| 31 | + | ||
| 32 | + public IotModbusFrameDecoder(int customFunctionCode) { | ||
| 33 | + this.customFunctionCode = customFunctionCode; | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 创建带自动帧格式检测的 RecordParser | ||
| 38 | + * | ||
| 39 | + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式) | ||
| 40 | + * @return RecordParser 实例 | ||
| 41 | + */ | ||
| 42 | + public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler) { | ||
| 43 | + // 先创建一个 RecordParser:使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测 | ||
| 44 | + RecordParser parser = RecordParser.newFixed(6); | ||
| 45 | + parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler)); | ||
| 46 | + return parser; | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + // ==================== 帧解码 ==================== | ||
| 50 | + | ||
| 51 | + /** | ||
| 52 | + * 解码响应帧(拆包后的完整帧 byte[]) | ||
| 53 | + * | ||
| 54 | + * @param data 完整帧字节数组 | ||
| 55 | + * @param format 帧格式 | ||
| 56 | + * @return 解码后的 IotModbusFrame | ||
| 57 | + */ | ||
| 58 | + private IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) { | ||
| 59 | + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { | ||
| 60 | + return decodeTcpResponse(data); | ||
| 61 | + } else { | ||
| 62 | + return decodeRtuResponse(data); | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + /** | ||
| 67 | + * 解码 MODBUS_TCP 响应 | ||
| 68 | + * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...] | ||
| 69 | + */ | ||
| 70 | + private IotModbusFrame decodeTcpResponse(byte[] data) { | ||
| 71 | + if (data.length < 8) { | ||
| 72 | + log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length); | ||
| 73 | + return null; | ||
| 74 | + } | ||
| 75 | + ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); | ||
| 76 | + int transactionId = buf.getShort() & 0xFFFF; | ||
| 77 | + buf.getShort(); // protocolId:固定 0x0000,Modbus 协议标识 | ||
| 78 | + buf.getShort(); // length:后续字节数(UnitId + PDU),拆包阶段已使用 | ||
| 79 | + int slaveId = buf.get() & 0xFF; | ||
| 80 | + int functionCode = buf.get() & 0xFF; | ||
| 81 | + // 提取 PDU 数据(从 functionCode 之后到末尾) | ||
| 82 | + byte[] pdu = new byte[data.length - 8]; | ||
| 83 | + System.arraycopy(data, 8, pdu, 0, pdu.length); | ||
| 84 | + | ||
| 85 | + return buildFrame(slaveId, functionCode, pdu, transactionId); | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + /** | ||
| 89 | + * 解码 MODBUS_RTU 响应 | ||
| 90 | + * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)] | ||
| 91 | + */ | ||
| 92 | + private IotModbusFrame decodeRtuResponse(byte[] data) { | ||
| 93 | + if (data.length < 4) { | ||
| 94 | + log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length); | ||
| 95 | + return null; | ||
| 96 | + } | ||
| 97 | + // 校验 CRC | ||
| 98 | + if (!IotModbusUtils.verifyCrc16(data)) { | ||
| 99 | + log.warn("[decodeRtuResponse][CRC 校验失败]"); | ||
| 100 | + return null; | ||
| 101 | + } | ||
| 102 | + int slaveId = data[0] & 0xFF; | ||
| 103 | + int functionCode = data[1] & 0xFF; | ||
| 104 | + // PDU 数据(不含 slaveId、functionCode、CRC) | ||
| 105 | + byte[] pdu = new byte[data.length - 4]; | ||
| 106 | + System.arraycopy(data, 2, pdu, 0, pdu.length); | ||
| 107 | + | ||
| 108 | + return buildFrame(slaveId, functionCode, pdu, null); | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + /** | ||
| 112 | + * 构建 IotModbusFrame | ||
| 113 | + */ | ||
| 114 | + private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) { | ||
| 115 | + IotModbusFrame frame = new IotModbusFrame() | ||
| 116 | + .setSlaveId(slaveId) | ||
| 117 | + .setFunctionCode(functionCode) | ||
| 118 | + .setPdu(pdu) | ||
| 119 | + .setTransactionId(transactionId); | ||
| 120 | + // 异常响应 | ||
| 121 | + // TODO @AI:0x80 看看是不是要枚举; | ||
| 122 | + if ((functionCode & 0x80) != 0) { | ||
| 123 | + frame.setException(true); | ||
| 124 | + // TODO @AI:0x7f 看看是不是要枚举; | ||
| 125 | + frame.setFunctionCode(functionCode & 0x7F); | ||
| 126 | + if (pdu.length >= 1) { | ||
| 127 | + frame.setExceptionCode(pdu[0] & 0xFF); | ||
| 128 | + } | ||
| 129 | + return frame; | ||
| 130 | + } | ||
| 131 | + // 自定义功能码 | ||
| 132 | + if (functionCode == customFunctionCode) { | ||
| 133 | + // data 区格式:[byteCount(1)] [JSON data(N)] | ||
| 134 | + if (pdu.length >= 1) { | ||
| 135 | + int byteCount = pdu[0] & 0xFF; | ||
| 136 | + if (pdu.length >= 1 + byteCount) { | ||
| 137 | + frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8)); | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | + return frame; | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + // ==================== 拆包 Handler ==================== | ||
| 145 | + | ||
| 146 | + /** | ||
| 147 | + * 帧格式检测阶段 Handler(仅处理首包,探测后切换到对应的拆包 Handler) | ||
| 148 | + */ | ||
| 149 | + @SuppressWarnings("ClassCanBeRecord") | ||
| 150 | + @RequiredArgsConstructor | ||
| 151 | + private class DetectPhaseHandler implements Handler<Buffer> { | ||
| 152 | + | ||
| 153 | + private final RecordParser parser; | ||
| 154 | + private final int customFunctionCode; | ||
| 155 | + private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler; | ||
| 156 | + | ||
| 157 | + @Override | ||
| 158 | + public void handle(Buffer buffer) { | ||
| 159 | + // 检测帧格式:protocolId==0x0000 且 length 合法 → MODBUS_TCP,否则 → MODBUS_RTU | ||
| 160 | + byte[] bytes = buffer.getBytes(); | ||
| 161 | + int protocolId = ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); | ||
| 162 | + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); | ||
| 163 | + | ||
| 164 | + // 分别处理 MODBUS_TCP、MODBUS_RTU 两种情况 | ||
| 165 | + if (protocolId == 0x0000 && length >= 1 && length <= 253) { | ||
| 166 | + // MODBUS_TCP:切换到 TCP 拆包 Handler | ||
| 167 | + log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]"); | ||
| 168 | + TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler); | ||
| 169 | + parser.handler(tcpHandler); | ||
| 170 | + // 当前 bytes 就是 MBAP 的前 6 字节,直接交给 tcpHandler 处理 | ||
| 171 | + tcpHandler.handleFirstBytes(bytes); | ||
| 172 | + } else { | ||
| 173 | + // MODBUS_RTU:切换到 RTU 拆包 Handler | ||
| 174 | + log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]"); | ||
| 175 | + RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode); | ||
| 176 | + parser.handler(rtuHandler); | ||
| 177 | + // 当前 bytes 包含前 6 字节(slaveId + FC + 部分数据),交给 rtuHandler 处理 | ||
| 178 | + rtuHandler.handleFirstBytes(bytes); | ||
| 179 | + } | ||
| 180 | + } | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + /** | ||
| 184 | + * MODBUS_TCP 拆包 Handler(两阶段 RecordParser) | ||
| 185 | + * <p> | ||
| 186 | + * Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length | ||
| 187 | + * Phase 2: fixedSizeMode(length) → 读 unitId + PDU | ||
| 188 | + */ | ||
| 189 | + @RequiredArgsConstructor | ||
| 190 | + private class TcpFrameHandler implements Handler<Buffer> { | ||
| 191 | + | ||
| 192 | + private final RecordParser parser; | ||
| 193 | + private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler; | ||
| 194 | + | ||
| 195 | + private byte[] mbapHeader; | ||
| 196 | + private boolean waitingForBody = false; | ||
| 197 | + | ||
| 198 | + /** | ||
| 199 | + * 处理探测阶段传来的首帧 6 字节(即 MBAP 头) | ||
| 200 | + * | ||
| 201 | + * @param bytes 探测阶段消费的 6 字节 | ||
| 202 | + */ | ||
| 203 | + void handleFirstBytes(byte[] bytes) { | ||
| 204 | + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF); | ||
| 205 | + this.mbapHeader = bytes; | ||
| 206 | + this.waitingForBody = true; | ||
| 207 | + parser.fixedSizeMode(length); | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + @Override | ||
| 211 | + public void handle(Buffer buffer) { | ||
| 212 | + if (waitingForBody) { | ||
| 213 | + // Phase 2: 收到 body(unitId + PDU) | ||
| 214 | + byte[] body = buffer.getBytes(); | ||
| 215 | + // 拼接完整帧:MBAP(6) + body | ||
| 216 | + byte[] fullFrame = new byte[mbapHeader.length + body.length]; | ||
| 217 | + System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length); | ||
| 218 | + System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length); | ||
| 219 | + // 解码并回调 | ||
| 220 | + IotModbusFrame frame = decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP); | ||
| 221 | + if (frame != null) { | ||
| 222 | + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_TCP); | ||
| 223 | + } | ||
| 224 | + // 切回 Phase 1 | ||
| 225 | + waitingForBody = false; | ||
| 226 | + mbapHeader = null; | ||
| 227 | + parser.fixedSizeMode(6); | ||
| 228 | + } else { | ||
| 229 | + // Phase 1: 收到 MBAP 头 6 字节 | ||
| 230 | + byte[] header = buffer.getBytes(); | ||
| 231 | + int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF); | ||
| 232 | + if (length < 1 || length > 253) { | ||
| 233 | + log.warn("[TcpFrameHandler][MBAP Length 异常: {}]", length); | ||
| 234 | + parser.fixedSizeMode(6); | ||
| 235 | + return; | ||
| 236 | + } | ||
| 237 | + this.mbapHeader = header; | ||
| 238 | + this.waitingForBody = true; | ||
| 239 | + parser.fixedSizeMode(length); | ||
| 240 | + } | ||
| 241 | + } | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + /** | ||
| 245 | + * MODBUS_RTU 拆包 Handler(功能码驱动的状态机) | ||
| 246 | + * <p> | ||
| 247 | + * 状态机流程: | ||
| 248 | + * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode | ||
| 249 | + * Phase 2: 根据 functionCode 确定剩余长度: | ||
| 250 | + * - 异常响应 (FC & 0x80):fixedSizeMode(3) → exceptionCode(1) + CRC(2) | ||
| 251 | + * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) | ||
| 252 | + * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) | ||
| 253 | + * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) | ||
| 254 | + */ | ||
| 255 | + @RequiredArgsConstructor | ||
| 256 | + private class RtuFrameHandler implements Handler<Buffer> { | ||
| 257 | + | ||
| 258 | + private static final int STATE_HEADER = 0; | ||
| 259 | + private static final int STATE_EXCEPTION_BODY = 1; | ||
| 260 | + private static final int STATE_READ_BYTE_COUNT = 2; | ||
| 261 | + private static final int STATE_READ_DATA = 3; | ||
| 262 | + private static final int STATE_WRITE_BODY = 4; | ||
| 263 | + | ||
| 264 | + private final RecordParser parser; | ||
| 265 | + private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler; | ||
| 266 | + private final int customFunctionCode; | ||
| 267 | + | ||
| 268 | + private int state = STATE_HEADER; | ||
| 269 | + private byte slaveId; | ||
| 270 | + private byte functionCode; | ||
| 271 | + private byte byteCount; | ||
| 272 | + private Buffer pendingData; | ||
| 273 | + private int expectedDataLen; | ||
| 274 | + | ||
| 275 | + /** | ||
| 276 | + * 处理探测阶段传来的首帧 6 字节 | ||
| 277 | + * <p> | ||
| 278 | + * 由于 RTU 首帧被探测阶段消费了 6 字节,这里需要从中提取 slaveId + FC 并根据 FC 处理剩余数据 | ||
| 279 | + * | ||
| 280 | + * @param bytes 探测阶段消费的 6 字节:[slaveId][FC][...4 bytes...] | ||
| 281 | + */ | ||
| 282 | + void handleFirstBytes(byte[] bytes) { | ||
| 283 | + this.slaveId = bytes[0]; | ||
| 284 | + this.functionCode = bytes[1]; | ||
| 285 | + int fc = functionCode & 0xFF; | ||
| 286 | + if ((fc & 0x80) != 0) { | ||
| 287 | + // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节 | ||
| 288 | + // 已有 6 字节(多 1 字节),取前 5 字节组装 | ||
| 289 | + Buffer frame = Buffer.buffer(5); | ||
| 290 | + frame.appendByte(slaveId); | ||
| 291 | + frame.appendByte(functionCode); | ||
| 292 | + frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC | ||
| 293 | + emitFrame(frame); | ||
| 294 | + resetToHeader(); | ||
| 295 | + } else if (isReadResponse(fc) || fc == customFunctionCode) { | ||
| 296 | + // 读响应或自定义 FC:bytes[2] = byteCount | ||
| 297 | + this.byteCount = bytes[2]; | ||
| 298 | + int bc = byteCount & 0xFF; | ||
| 299 | + // 已有数据:bytes[3..5] = 3 字节 | ||
| 300 | + // 还需:byteCount + CRC(2) - 3 字节已有 | ||
| 301 | + int remaining = bc + 2 - 3; | ||
| 302 | + if (remaining <= 0) { | ||
| 303 | + // 数据已足够,组装完整帧 | ||
| 304 | + int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC | ||
| 305 | + Buffer frame = Buffer.buffer(totalLen); | ||
| 306 | + frame.appendByte(slaveId); | ||
| 307 | + frame.appendByte(functionCode); | ||
| 308 | + frame.appendByte(byteCount); | ||
| 309 | + frame.appendBytes(bytes, 3, bc + 2); // data + CRC | ||
| 310 | + emitFrame(frame); | ||
| 311 | + resetToHeader(); | ||
| 312 | + } else { | ||
| 313 | + // 需要继续读 | ||
| 314 | + state = STATE_READ_DATA; | ||
| 315 | + this.pendingData = Buffer.buffer(); | ||
| 316 | + this.pendingData.appendBytes(bytes, 3, 3); // 暂存已有的 3 字节 | ||
| 317 | + this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC | ||
| 318 | + parser.fixedSizeMode(remaining); | ||
| 319 | + } | ||
| 320 | + } else if (isWriteResponse(fc)) { | ||
| 321 | + // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节 | ||
| 322 | + // 已有 6 字节,还需 2 字节 | ||
| 323 | + state = STATE_WRITE_BODY; | ||
| 324 | + this.pendingData = Buffer.buffer(); | ||
| 325 | + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节 | ||
| 326 | + parser.fixedSizeMode(2); // 还需 2 字节(CRC) | ||
| 327 | + } else { | ||
| 328 | + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); | ||
| 329 | + resetToHeader(); | ||
| 330 | + } | ||
| 331 | + } | ||
| 332 | + | ||
| 333 | + @Override | ||
| 334 | + public void handle(Buffer buffer) { | ||
| 335 | + switch (state) { | ||
| 336 | + case STATE_HEADER: | ||
| 337 | + handleHeader(buffer); | ||
| 338 | + break; | ||
| 339 | + case STATE_EXCEPTION_BODY: | ||
| 340 | + handleExceptionBody(buffer); | ||
| 341 | + break; | ||
| 342 | + case STATE_READ_BYTE_COUNT: | ||
| 343 | + handleReadByteCount(buffer); | ||
| 344 | + break; | ||
| 345 | + case STATE_READ_DATA: | ||
| 346 | + handleReadData(buffer); | ||
| 347 | + break; | ||
| 348 | + case STATE_WRITE_BODY: | ||
| 349 | + handleWriteBody(buffer); | ||
| 350 | + break; | ||
| 351 | + default: | ||
| 352 | + resetToHeader(); | ||
| 353 | + } | ||
| 354 | + } | ||
| 355 | + | ||
| 356 | + private void handleHeader(Buffer buffer) { | ||
| 357 | + byte[] header = buffer.getBytes(); | ||
| 358 | + this.slaveId = header[0]; | ||
| 359 | + this.functionCode = header[1]; | ||
| 360 | + int fc = functionCode & 0xFF; | ||
| 361 | + if ((fc & 0x80) != 0) { | ||
| 362 | + // 异常响应 | ||
| 363 | + state = STATE_EXCEPTION_BODY; | ||
| 364 | + parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) | ||
| 365 | + } else if (isReadResponse(fc) || fc == customFunctionCode) { | ||
| 366 | + // 读响应或自定义 FC | ||
| 367 | + state = STATE_READ_BYTE_COUNT; | ||
| 368 | + parser.fixedSizeMode(1); // byteCount | ||
| 369 | + } else if (isWriteResponse(fc)) { | ||
| 370 | + // 写响应 | ||
| 371 | + state = STATE_WRITE_BODY; | ||
| 372 | + pendingData = Buffer.buffer(); | ||
| 373 | + parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2) | ||
| 374 | + } else { | ||
| 375 | + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); | ||
| 376 | + resetToHeader(); | ||
| 377 | + } | ||
| 378 | + } | ||
| 379 | + | ||
| 380 | + private void handleExceptionBody(Buffer buffer) { | ||
| 381 | + // buffer = exceptionCode(1) + CRC(2) | ||
| 382 | + Buffer frame = Buffer.buffer(); | ||
| 383 | + frame.appendByte(slaveId); | ||
| 384 | + frame.appendByte(functionCode); | ||
| 385 | + frame.appendBuffer(buffer); | ||
| 386 | + emitFrame(frame); | ||
| 387 | + resetToHeader(); | ||
| 388 | + } | ||
| 389 | + | ||
| 390 | + private void handleReadByteCount(Buffer buffer) { | ||
| 391 | + this.byteCount = buffer.getByte(0); | ||
| 392 | + int bc = byteCount & 0xFF; | ||
| 393 | + state = STATE_READ_DATA; | ||
| 394 | + pendingData = Buffer.buffer(); | ||
| 395 | + expectedDataLen = bc + 2; // data(bc) + CRC(2) | ||
| 396 | + parser.fixedSizeMode(expectedDataLen); | ||
| 397 | + } | ||
| 398 | + | ||
| 399 | + private void handleReadData(Buffer buffer) { | ||
| 400 | + pendingData.appendBuffer(buffer); | ||
| 401 | + if (pendingData.length() >= expectedDataLen) { | ||
| 402 | + // 组装完整帧 | ||
| 403 | + Buffer frame = Buffer.buffer(); | ||
| 404 | + frame.appendByte(slaveId); | ||
| 405 | + frame.appendByte(functionCode); | ||
| 406 | + frame.appendByte(byteCount); | ||
| 407 | + frame.appendBuffer(pendingData); | ||
| 408 | + emitFrame(frame); | ||
| 409 | + resetToHeader(); | ||
| 410 | + } | ||
| 411 | + // 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode) | ||
| 412 | + } | ||
| 413 | + | ||
| 414 | + private void handleWriteBody(Buffer buffer) { | ||
| 415 | + pendingData.appendBuffer(buffer); | ||
| 416 | + // 完整帧 | ||
| 417 | + Buffer frame = Buffer.buffer(); | ||
| 418 | + frame.appendByte(slaveId); | ||
| 419 | + frame.appendByte(functionCode); | ||
| 420 | + frame.appendBuffer(pendingData); | ||
| 421 | + emitFrame(frame); | ||
| 422 | + resetToHeader(); | ||
| 423 | + } | ||
| 424 | + | ||
| 425 | + /** | ||
| 426 | + * 发射完整帧:解码并回调 | ||
| 427 | + */ | ||
| 428 | + private void emitFrame(Buffer frameBuffer) { | ||
| 429 | + IotModbusFrame frame = decodeResponse(frameBuffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); | ||
| 430 | + if (frame != null) { | ||
| 431 | + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_RTU); | ||
| 432 | + } | ||
| 433 | + } | ||
| 434 | + | ||
| 435 | + private void resetToHeader() { | ||
| 436 | + state = STATE_HEADER; | ||
| 437 | + pendingData = null; | ||
| 438 | + parser.fixedSizeMode(2); // slaveId + FC | ||
| 439 | + } | ||
| 440 | + | ||
| 441 | + // TODO @AI:可以抽到 IotModbusUtils 里? | ||
| 442 | + private boolean isReadResponse(int fc) { | ||
| 443 | + return fc >= 1 && fc <= 4; | ||
| 444 | + } | ||
| 445 | + | ||
| 446 | + private boolean isWriteResponse(int fc) { | ||
| 447 | + return fc == 5 || fc == 6 || fc == 15 || fc == 16; | ||
| 448 | + } | ||
| 449 | + } | ||
| 450 | + | ||
| 451 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | ||
| 4 | +import lombok.RequiredArgsConstructor; | ||
| 5 | +import lombok.extern.slf4j.Slf4j; | ||
| 6 | + | ||
| 7 | +import java.nio.charset.StandardCharsets; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * IoT Modbus 帧编码器 | ||
| 11 | + * <p> | ||
| 12 | + * 负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。 | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +@RequiredArgsConstructor | ||
| 17 | +@Slf4j | ||
| 18 | +public class IotModbusFrameEncoder { | ||
| 19 | + | ||
| 20 | + private final int customFunctionCode; | ||
| 21 | + | ||
| 22 | + // ==================== 编码 ==================== | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 编码读请求 | ||
| 26 | + * | ||
| 27 | + * @param slaveId 从站地址 | ||
| 28 | + * @param functionCode 功能码 | ||
| 29 | + * @param startAddress 起始寄存器地址 | ||
| 30 | + * @param quantity 寄存器数量 | ||
| 31 | + * @param format 帧格式 | ||
| 32 | + * @param transactionId 事务 ID(TCP 模式下使用) | ||
| 33 | + * @return 编码后的字节数组 | ||
| 34 | + */ | ||
| 35 | + public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity, | ||
| 36 | + IotModbusFrameFormatEnum format, int transactionId) { | ||
| 37 | + // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)] | ||
| 38 | + byte[] pdu = new byte[5]; | ||
| 39 | + pdu[0] = (byte) functionCode; | ||
| 40 | + pdu[1] = (byte) ((startAddress >> 8) & 0xFF); | ||
| 41 | + pdu[2] = (byte) (startAddress & 0xFF); | ||
| 42 | + pdu[3] = (byte) ((quantity >> 8) & 0xFF); | ||
| 43 | + pdu[4] = (byte) (quantity & 0xFF); | ||
| 44 | + return wrapFrame(slaveId, pdu, format, transactionId); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + /** | ||
| 48 | + * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) | ||
| 49 | + * | ||
| 50 | + * @param slaveId 从站地址 | ||
| 51 | + * @param functionCode 功能码 | ||
| 52 | + * @param address 寄存器地址 | ||
| 53 | + * @param value 值 | ||
| 54 | + * @param format 帧格式 | ||
| 55 | + * @param transactionId 事务 ID | ||
| 56 | + * @return 编码后的字节数组 | ||
| 57 | + */ | ||
| 58 | + public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value, | ||
| 59 | + IotModbusFrameFormatEnum format, int transactionId) { | ||
| 60 | + // PDU: [FC(1)] [Address(2)] [Value(2)] | ||
| 61 | + byte[] pdu = new byte[5]; | ||
| 62 | + pdu[0] = (byte) functionCode; | ||
| 63 | + pdu[1] = (byte) ((address >> 8) & 0xFF); | ||
| 64 | + pdu[2] = (byte) (address & 0xFF); | ||
| 65 | + pdu[3] = (byte) ((value >> 8) & 0xFF); | ||
| 66 | + pdu[4] = (byte) (value & 0xFF); | ||
| 67 | + return wrapFrame(slaveId, pdu, format, transactionId); | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + /** | ||
| 71 | + * 编码写多个寄存器请求(FC16) | ||
| 72 | + * | ||
| 73 | + * @param slaveId 从站地址 | ||
| 74 | + * @param address 起始地址 | ||
| 75 | + * @param values 值数组 | ||
| 76 | + * @param format 帧格式 | ||
| 77 | + * @param transactionId 事务 ID | ||
| 78 | + * @return 编码后的字节数组 | ||
| 79 | + */ | ||
| 80 | + public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values, | ||
| 81 | + IotModbusFrameFormatEnum format, int transactionId) { | ||
| 82 | + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)] | ||
| 83 | + int quantity = values.length; | ||
| 84 | + int byteCount = quantity * 2; | ||
| 85 | + byte[] pdu = new byte[6 + byteCount]; | ||
| 86 | + pdu[0] = (byte) 16; // FC16 | ||
| 87 | + pdu[1] = (byte) ((address >> 8) & 0xFF); | ||
| 88 | + pdu[2] = (byte) (address & 0xFF); | ||
| 89 | + pdu[3] = (byte) ((quantity >> 8) & 0xFF); | ||
| 90 | + pdu[4] = (byte) (quantity & 0xFF); | ||
| 91 | + pdu[5] = (byte) byteCount; | ||
| 92 | + for (int i = 0; i < quantity; i++) { | ||
| 93 | + pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF); | ||
| 94 | + pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF); | ||
| 95 | + } | ||
| 96 | + return wrapFrame(slaveId, pdu, format, transactionId); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + /** | ||
| 100 | + * 编码自定义功能码帧(认证响应等) | ||
| 101 | + * | ||
| 102 | + * @param slaveId 从站地址 | ||
| 103 | + * @param jsonData JSON 数据 | ||
| 104 | + * @param format 帧格式 | ||
| 105 | + * @param transactionId 事务 ID | ||
| 106 | + * @return 编码后的字节数组 | ||
| 107 | + */ | ||
| 108 | + public byte[] encodeCustomFrame(int slaveId, String jsonData, | ||
| 109 | + IotModbusFrameFormatEnum format, int transactionId) { | ||
| 110 | + byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8); | ||
| 111 | + // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)] | ||
| 112 | + byte[] pdu = new byte[2 + jsonBytes.length]; | ||
| 113 | + pdu[0] = (byte) customFunctionCode; | ||
| 114 | + pdu[1] = (byte) jsonBytes.length; | ||
| 115 | + System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length); | ||
| 116 | + return wrapFrame(slaveId, pdu, format, transactionId); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + // ==================== 帧封装 ==================== | ||
| 120 | + | ||
| 121 | + /** | ||
| 122 | + * 将 PDU 封装为完整帧 | ||
| 123 | + * | ||
| 124 | + * @param slaveId 从站地址 | ||
| 125 | + * @param pdu PDU 数据(含 functionCode) | ||
| 126 | + * @param format 帧格式 | ||
| 127 | + * @param transactionId 事务 ID(TCP 模式下使用) | ||
| 128 | + * @return 完整帧字节数组 | ||
| 129 | + */ | ||
| 130 | + private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, int transactionId) { | ||
| 131 | + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { | ||
| 132 | + return wrapTcpFrame(slaveId, pdu, transactionId); | ||
| 133 | + } else { | ||
| 134 | + return wrapRtuFrame(slaveId, pdu); | ||
| 135 | + } | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + /** | ||
| 139 | + * 封装 MODBUS_TCP 帧 | ||
| 140 | + * [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...] | ||
| 141 | + */ | ||
| 142 | + private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) { | ||
| 143 | + int length = 1 + pdu.length; // UnitId + PDU | ||
| 144 | + byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU | ||
| 145 | + // MBAP Header | ||
| 146 | + frame[0] = (byte) ((transactionId >> 8) & 0xFF); | ||
| 147 | + frame[1] = (byte) (transactionId & 0xFF); | ||
| 148 | + frame[2] = 0; // Protocol ID high | ||
| 149 | + frame[3] = 0; // Protocol ID low | ||
| 150 | + frame[4] = (byte) ((length >> 8) & 0xFF); | ||
| 151 | + frame[5] = (byte) (length & 0xFF); | ||
| 152 | + // Unit ID | ||
| 153 | + frame[6] = (byte) slaveId; | ||
| 154 | + // PDU | ||
| 155 | + System.arraycopy(pdu, 0, frame, 7, pdu.length); | ||
| 156 | + return frame; | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + /** | ||
| 160 | + * 封装 MODBUS_RTU 帧 | ||
| 161 | + * [SlaveId(1)] [PDU...] [CRC(2)] | ||
| 162 | + */ | ||
| 163 | + private byte[] wrapRtuFrame(int slaveId, byte[] pdu) { | ||
| 164 | + byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC | ||
| 165 | + frame[0] = (byte) slaveId; | ||
| 166 | + System.arraycopy(pdu, 0, frame, 1, pdu.length); | ||
| 167 | + // 计算并追加 CRC16 | ||
| 168 | + int crc = IotModbusUtils.calculateCrc16(frame, frame.length - 2); | ||
| 169 | + frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low | ||
| 170 | + frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High | ||
| 171 | + return frame; | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | ||
| 4 | -import io.vertx.core.Handler; | ||
| 5 | -import io.vertx.core.buffer.Buffer; | ||
| 6 | -import io.vertx.core.parsetools.RecordParser; | ||
| 7 | -import lombok.extern.slf4j.Slf4j; | ||
| 8 | - | ||
| 9 | -import java.util.function.Consumer; | ||
| 10 | - | ||
| 11 | -// TODO @AI:看看是不是不要搞成 factory,而是直接 new;(可以一起讨论下) | ||
| 12 | -/** | ||
| 13 | - * IoT Modbus RecordParser 工厂 | ||
| 14 | - * <p> | ||
| 15 | - * 创建带自动帧格式检测的 RecordParser: | ||
| 16 | - * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU | ||
| 17 | - * 2. 检测后自动切换到对应的拆包模式 | ||
| 18 | - * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动) | ||
| 19 | - * - MODBUS_RTU:功能码驱动的状态机 | ||
| 20 | - * | ||
| 21 | - * @author 芋道源码 | ||
| 22 | - */ | ||
| 23 | -@Slf4j | ||
| 24 | -public class IotModbusRecordParserFactory { | ||
| 25 | - | ||
| 26 | - /** | ||
| 27 | - * 创建带自动帧格式检测的 RecordParser | ||
| 28 | - * | ||
| 29 | - * @param customFunctionCode 自定义功能码 | ||
| 30 | - * @param frameHandler 完整帧回调 | ||
| 31 | - * @param onFormatDetected 帧格式检测回调 | ||
| 32 | - * @return RecordParser 实例 | ||
| 33 | - */ | ||
| 34 | - public static RecordParser create(int customFunctionCode, | ||
| 35 | - Handler<Buffer> frameHandler, | ||
| 36 | - Consumer<IotModbusFrameFormatEnum> onFormatDetected) { | ||
| 37 | - // 先创建一个 RecordParser,使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测 | ||
| 38 | - // TODO @AI:最小需要 6 个字节么?有可能更小的情况下,就探测出来?! | ||
| 39 | - RecordParser parser = RecordParser.newFixed(6); | ||
| 40 | - parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, onFormatDetected)); | ||
| 41 | - return parser; | ||
| 42 | - } | ||
| 43 | - | ||
| 44 | - /** | ||
| 45 | - * 帧格式检测阶段 Handler | ||
| 46 | - */ | ||
| 47 | - @SuppressWarnings("ClassCanBeRecord") | ||
| 48 | - private static class DetectPhaseHandler implements Handler<Buffer> { | ||
| 49 | - | ||
| 50 | - private final RecordParser parser; | ||
| 51 | - private final int customFunctionCode; | ||
| 52 | - private final Handler<Buffer> frameHandler; | ||
| 53 | - private final Consumer<IotModbusFrameFormatEnum> onFormatDetected; | ||
| 54 | - | ||
| 55 | - // TODO @AI:简化构造方法,使用 lombok; | ||
| 56 | - DetectPhaseHandler(RecordParser parser, int customFunctionCode, | ||
| 57 | - Handler<Buffer> frameHandler, | ||
| 58 | - Consumer<IotModbusFrameFormatEnum> onFormatDetected) { | ||
| 59 | - this.parser = parser; | ||
| 60 | - this.customFunctionCode = customFunctionCode; | ||
| 61 | - this.frameHandler = frameHandler; | ||
| 62 | - this.onFormatDetected = onFormatDetected; | ||
| 63 | - } | ||
| 64 | - | ||
| 65 | - @Override | ||
| 66 | - public void handle(Buffer buffer) { | ||
| 67 | - byte[] header = buffer.getBytes(); | ||
| 68 | - // 检测:byte[2]==0x00 && byte[3]==0x00 && 1<=length<=253 | ||
| 69 | - int protocolId = ((header[2] & 0xFF) << 8) | (header[3] & 0xFF); | ||
| 70 | - int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF); | ||
| 71 | - | ||
| 72 | - if (protocolId == 0x0000 && length >= 1 && length <= 253) { | ||
| 73 | - // MODBUS_TCP | ||
| 74 | - log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]"); | ||
| 75 | - onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_TCP); | ||
| 76 | - // 切换到 TCP 拆包模式,处理当前首帧 | ||
| 77 | - TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler); | ||
| 78 | - parser.handler(tcpHandler); | ||
| 79 | - // 当前 header 是 MBAP 的前 6 字节,需要继续读 length 字节 | ||
| 80 | - tcpHandler.handleMbapHeader(header, length); | ||
| 81 | - } else { | ||
| 82 | - // MODBUS_RTU | ||
| 83 | - log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]"); | ||
| 84 | - onFormatDetected.accept(IotModbusFrameFormatEnum.MODBUS_RTU); | ||
| 85 | - // 切换到 RTU 拆包模式,处理当前首帧 | ||
| 86 | - RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, customFunctionCode, frameHandler); | ||
| 87 | - parser.handler(rtuHandler); | ||
| 88 | - // 当前 header 包含前 6 字节(slaveId + FC + 部分数据),需要拼接处理 | ||
| 89 | - rtuHandler.handleInitialBytes(header); | ||
| 90 | - } | ||
| 91 | - } | ||
| 92 | - } | ||
| 93 | - | ||
| 94 | - /** | ||
| 95 | - * MODBUS_TCP 拆包 Handler(两阶段 RecordParser) | ||
| 96 | - * Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length | ||
| 97 | - * Phase 2: fixedSizeMode(length) → 读 unitId + PDU | ||
| 98 | - */ | ||
| 99 | - private static class TcpFrameHandler implements Handler<Buffer> { | ||
| 100 | - | ||
| 101 | - private final RecordParser parser; | ||
| 102 | - private final Handler<Buffer> frameHandler; | ||
| 103 | - private byte[] mbapHeader; | ||
| 104 | - private boolean waitingForBody = false; | ||
| 105 | - | ||
| 106 | - // TODO @AI:lombok | ||
| 107 | - TcpFrameHandler(RecordParser parser, Handler<Buffer> frameHandler) { | ||
| 108 | - this.parser = parser; | ||
| 109 | - this.frameHandler = frameHandler; | ||
| 110 | - } | ||
| 111 | - | ||
| 112 | - /** | ||
| 113 | - * 处理首帧的 MBAP 头 | ||
| 114 | - */ | ||
| 115 | - void handleMbapHeader(byte[] header, int length) { | ||
| 116 | - this.mbapHeader = header; | ||
| 117 | - this.waitingForBody = true; | ||
| 118 | - parser.fixedSizeMode(length); | ||
| 119 | - } | ||
| 120 | - | ||
| 121 | - @Override | ||
| 122 | - public void handle(Buffer buffer) { | ||
| 123 | - if (waitingForBody) { | ||
| 124 | - // Phase 2: 收到 body(unitId + PDU) | ||
| 125 | - byte[] body = buffer.getBytes(); | ||
| 126 | - // 拼接完整帧:MBAP(6) + body | ||
| 127 | - Buffer frame = Buffer.buffer(mbapHeader.length + body.length); | ||
| 128 | - frame.appendBytes(mbapHeader); | ||
| 129 | - frame.appendBytes(body); | ||
| 130 | - frameHandler.handle(frame); | ||
| 131 | - // 切回 Phase 1 | ||
| 132 | - waitingForBody = false; | ||
| 133 | - mbapHeader = null; | ||
| 134 | - parser.fixedSizeMode(6); | ||
| 135 | - } else { | ||
| 136 | - // Phase 1: 收到 MBAP 头 6 字节 | ||
| 137 | - byte[] header = buffer.getBytes(); | ||
| 138 | - int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF); | ||
| 139 | - if (length < 1 || length > 253) { | ||
| 140 | - log.warn("[TcpFrameHandler][MBAP Length 异常: {}]", length); | ||
| 141 | - parser.fixedSizeMode(6); | ||
| 142 | - return; | ||
| 143 | - } | ||
| 144 | - this.mbapHeader = header; | ||
| 145 | - this.waitingForBody = true; | ||
| 146 | - parser.fixedSizeMode(length); | ||
| 147 | - } | ||
| 148 | - } | ||
| 149 | - } | ||
| 150 | - | ||
| 151 | - /** | ||
| 152 | - * MODBUS_RTU 拆包 Handler(功能码驱动的状态机) | ||
| 153 | - * <p> | ||
| 154 | - * 状态机流程: | ||
| 155 | - * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode | ||
| 156 | - * Phase 2: 根据 functionCode 确定剩余长度: | ||
| 157 | - * - 异常响应 (FC & 0x80):fixedSizeMode(3) → exceptionCode(1) + CRC(2) | ||
| 158 | - * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) | ||
| 159 | - * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) | ||
| 160 | - * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) | ||
| 161 | - */ | ||
| 162 | - private static class RtuFrameHandler implements Handler<Buffer> { | ||
| 163 | - | ||
| 164 | - private static final int STATE_HEADER = 0; | ||
| 165 | - private static final int STATE_EXCEPTION_BODY = 1; | ||
| 166 | - private static final int STATE_READ_BYTE_COUNT = 2; | ||
| 167 | - private static final int STATE_READ_DATA = 3; | ||
| 168 | - private static final int STATE_WRITE_BODY = 4; | ||
| 169 | - | ||
| 170 | - private final RecordParser parser; | ||
| 171 | - private final int customFunctionCode; | ||
| 172 | - private final Handler<Buffer> frameHandler; | ||
| 173 | - | ||
| 174 | - private int state = STATE_HEADER; | ||
| 175 | - private byte slaveId; | ||
| 176 | - private byte functionCode; | ||
| 177 | - private byte byteCount; | ||
| 178 | - | ||
| 179 | - // TODO @AI:lombok | ||
| 180 | - RtuFrameHandler(RecordParser parser, int customFunctionCode, Handler<Buffer> frameHandler) { | ||
| 181 | - this.parser = parser; | ||
| 182 | - this.customFunctionCode = customFunctionCode; | ||
| 183 | - this.frameHandler = frameHandler; | ||
| 184 | - } | ||
| 185 | - | ||
| 186 | - /** | ||
| 187 | - * 处理首帧检测阶段传来的初始 6 字节 | ||
| 188 | - * 由于 RTU 首帧跳过了格式检测,我们需要拼接处理 | ||
| 189 | - */ | ||
| 190 | - void handleInitialBytes(byte[] initialBytes) { | ||
| 191 | - // initialBytes 包含 6 字节:[slaveId][FC][...4 bytes...] | ||
| 192 | - this.slaveId = initialBytes[0]; | ||
| 193 | - this.functionCode = initialBytes[1]; | ||
| 194 | - int fc = functionCode & 0xFF; | ||
| 195 | - | ||
| 196 | - // 根据功能码,确定还需要多少字节 | ||
| 197 | - if ((fc & 0x80) != 0) { | ||
| 198 | - // 异常响应:还需要 exceptionCode(1) + CRC(2) = 3 字节 | ||
| 199 | - // 我们已经有 4 字节剩余(initialBytes[2..5]),足够 | ||
| 200 | - // 拼接完整帧并交付 | ||
| 201 | - // 完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 | ||
| 202 | - Buffer frame = Buffer.buffer(5); | ||
| 203 | - frame.appendByte(slaveId); | ||
| 204 | - frame.appendByte(functionCode); | ||
| 205 | - frame.appendBytes(initialBytes, 2, 3); // exceptionCode + CRC | ||
| 206 | - frameHandler.handle(frame); | ||
| 207 | - // 剩余 1 字节需要留给下一帧,但 RecordParser 不支持回推 | ||
| 208 | - // 简化处理:重置状态,开始读下一帧 | ||
| 209 | - resetToHeader(); | ||
| 210 | - } else if (isReadResponse(fc) || fc == customFunctionCode) { | ||
| 211 | - // 读响应或自定义 FC:initialBytes[2] = byteCount | ||
| 212 | - this.byteCount = initialBytes[2]; | ||
| 213 | - int bc = byteCount & 0xFF; | ||
| 214 | - // 已有数据:initialBytes[3..5] = 3 字节 | ||
| 215 | - // 还需:byteCount + CRC(2) - 3 字节已有 | ||
| 216 | - int remaining = bc + 2 - 3; | ||
| 217 | - if (remaining <= 0) { | ||
| 218 | - // 数据已足够,组装完整帧 | ||
| 219 | - int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC | ||
| 220 | - Buffer frame = Buffer.buffer(totalLen); | ||
| 221 | - frame.appendByte(slaveId); | ||
| 222 | - frame.appendByte(functionCode); | ||
| 223 | - frame.appendByte(byteCount); | ||
| 224 | - frame.appendBytes(initialBytes, 3, bc + 2); // data + CRC | ||
| 225 | - frameHandler.handle(frame); | ||
| 226 | - resetToHeader(); | ||
| 227 | - } else { | ||
| 228 | - // 需要继续读 | ||
| 229 | - state = STATE_READ_DATA; | ||
| 230 | - // 保存已有数据片段 | ||
| 231 | - parser.fixedSizeMode(remaining); | ||
| 232 | - // 在 handle() 中需要拼接 initialBytes[3..5] + 新读取的数据 | ||
| 233 | - // 为了简化,我们用一个 Buffer 暂存 | ||
| 234 | - this.pendingData = Buffer.buffer(); | ||
| 235 | - this.pendingData.appendBytes(initialBytes, 3, 3); | ||
| 236 | - this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC | ||
| 237 | - } | ||
| 238 | - } else if (isWriteResponse(fc)) { | ||
| 239 | - // 写响应:FC05/06/15/16,总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 | ||
| 240 | - // 已有 6 字节,还需 2 字节 | ||
| 241 | - state = STATE_WRITE_BODY; | ||
| 242 | - this.pendingData = Buffer.buffer(); | ||
| 243 | - this.pendingData.appendBytes(initialBytes, 2, 4); // 4 bytes already | ||
| 244 | - parser.fixedSizeMode(2); // need 2 more bytes (CRC) | ||
| 245 | - } else { | ||
| 246 | - log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); | ||
| 247 | - resetToHeader(); | ||
| 248 | - } | ||
| 249 | - } | ||
| 250 | - | ||
| 251 | - private Buffer pendingData; | ||
| 252 | - private int expectedDataLen; | ||
| 253 | - | ||
| 254 | - @Override | ||
| 255 | - public void handle(Buffer buffer) { | ||
| 256 | - switch (state) { | ||
| 257 | - case STATE_HEADER: | ||
| 258 | - handleHeader(buffer); | ||
| 259 | - break; | ||
| 260 | - case STATE_EXCEPTION_BODY: | ||
| 261 | - handleExceptionBody(buffer); | ||
| 262 | - break; | ||
| 263 | - case STATE_READ_BYTE_COUNT: | ||
| 264 | - handleReadByteCount(buffer); | ||
| 265 | - break; | ||
| 266 | - case STATE_READ_DATA: | ||
| 267 | - handleReadData(buffer); | ||
| 268 | - break; | ||
| 269 | - case STATE_WRITE_BODY: | ||
| 270 | - handleWriteBody(buffer); | ||
| 271 | - break; | ||
| 272 | - default: | ||
| 273 | - resetToHeader(); | ||
| 274 | - } | ||
| 275 | - } | ||
| 276 | - | ||
| 277 | - private void handleHeader(Buffer buffer) { | ||
| 278 | - byte[] header = buffer.getBytes(); | ||
| 279 | - this.slaveId = header[0]; | ||
| 280 | - this.functionCode = header[1]; | ||
| 281 | - int fc = functionCode & 0xFF; | ||
| 282 | - | ||
| 283 | - if ((fc & 0x80) != 0) { | ||
| 284 | - // 异常响应 | ||
| 285 | - state = STATE_EXCEPTION_BODY; | ||
| 286 | - parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) | ||
| 287 | - } else if (isReadResponse(fc) || fc == customFunctionCode) { | ||
| 288 | - // 读响应或自定义 FC | ||
| 289 | - state = STATE_READ_BYTE_COUNT; | ||
| 290 | - parser.fixedSizeMode(1); // byteCount | ||
| 291 | - } else if (isWriteResponse(fc)) { | ||
| 292 | - // 写响应 | ||
| 293 | - state = STATE_WRITE_BODY; | ||
| 294 | - pendingData = Buffer.buffer(); | ||
| 295 | - parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2) | ||
| 296 | - } else { | ||
| 297 | - log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc)); | ||
| 298 | - resetToHeader(); | ||
| 299 | - } | ||
| 300 | - } | ||
| 301 | - | ||
| 302 | - private void handleExceptionBody(Buffer buffer) { | ||
| 303 | - // buffer = exceptionCode(1) + CRC(2) | ||
| 304 | - Buffer frame = Buffer.buffer(); | ||
| 305 | - frame.appendByte(slaveId); | ||
| 306 | - frame.appendByte(functionCode); | ||
| 307 | - frame.appendBuffer(buffer); | ||
| 308 | - frameHandler.handle(frame); | ||
| 309 | - resetToHeader(); | ||
| 310 | - } | ||
| 311 | - | ||
| 312 | - private void handleReadByteCount(Buffer buffer) { | ||
| 313 | - this.byteCount = buffer.getByte(0); | ||
| 314 | - int bc = byteCount & 0xFF; | ||
| 315 | - state = STATE_READ_DATA; | ||
| 316 | - pendingData = Buffer.buffer(); | ||
| 317 | - expectedDataLen = bc + 2; // data(bc) + CRC(2) | ||
| 318 | - parser.fixedSizeMode(expectedDataLen); | ||
| 319 | - } | ||
| 320 | - | ||
| 321 | - private void handleReadData(Buffer buffer) { | ||
| 322 | - pendingData.appendBuffer(buffer); | ||
| 323 | - if (pendingData.length() >= expectedDataLen) { | ||
| 324 | - // 组装完整帧 | ||
| 325 | - Buffer frame = Buffer.buffer(); | ||
| 326 | - frame.appendByte(slaveId); | ||
| 327 | - frame.appendByte(functionCode); | ||
| 328 | - frame.appendByte(byteCount); | ||
| 329 | - frame.appendBuffer(pendingData); | ||
| 330 | - frameHandler.handle(frame); | ||
| 331 | - resetToHeader(); | ||
| 332 | - } | ||
| 333 | - // 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode) | ||
| 334 | - } | ||
| 335 | - | ||
| 336 | - private void handleWriteBody(Buffer buffer) { | ||
| 337 | - pendingData.appendBuffer(buffer); | ||
| 338 | - // 完整帧 | ||
| 339 | - Buffer frame = Buffer.buffer(); | ||
| 340 | - frame.appendByte(slaveId); | ||
| 341 | - frame.appendByte(functionCode); | ||
| 342 | - frame.appendBuffer(pendingData); | ||
| 343 | - frameHandler.handle(frame); | ||
| 344 | - resetToHeader(); | ||
| 345 | - } | ||
| 346 | - | ||
| 347 | - private void resetToHeader() { | ||
| 348 | - state = STATE_HEADER; | ||
| 349 | - pendingData = null; | ||
| 350 | - parser.fixedSizeMode(2); // slaveId + FC | ||
| 351 | - } | ||
| 352 | - | ||
| 353 | - private boolean isReadResponse(int fc) { | ||
| 354 | - return fc >= 1 && fc <= 4; | ||
| 355 | - } | ||
| 356 | - | ||
| 357 | - private boolean isWriteResponse(int fc) { | ||
| 358 | - return fc == 5 || fc == 6 || fc == 15 || fc == 16; | ||
| 359 | - } | ||
| 360 | - } | ||
| 361 | - | ||
| 362 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusResponseParser.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java
| @@ -3,14 +3,61 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | @@ -3,14 +3,61 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | ||
| 3 | import lombok.extern.slf4j.Slf4j; | 3 | import lombok.extern.slf4j.Slf4j; |
| 4 | 4 | ||
| 5 | /** | 5 | /** |
| 6 | - * IoT Modbus 响应值提取器 | 6 | + * IoT Modbus 工具类 |
| 7 | * <p> | 7 | * <p> |
| 8 | - * 从解码后的 IotModbusFrame 中提取寄存器值,用于后续的点位翻译。 | 8 | + * 提供: |
| 9 | + * 1. CRC-16/MODBUS 计算和校验 | ||
| 10 | + * 2. 从解码后的 IotModbusFrame 中提取寄存器值(用于后续的点位翻译) | ||
| 9 | * | 11 | * |
| 10 | * @author 芋道源码 | 12 | * @author 芋道源码 |
| 11 | */ | 13 | */ |
| 12 | @Slf4j | 14 | @Slf4j |
| 13 | -public class IotModbusResponseParser { | 15 | +public class IotModbusUtils { |
| 16 | + | ||
| 17 | + // TODO @AI:可以把 1、2、3、4、5 这些 fucntion code 在这里枚举下。 | ||
| 18 | + // TODO @AI:一些枚举 0x80 这些可以这里枚举; | ||
| 19 | + | ||
| 20 | + // ==================== CRC16 工具 ==================== | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 计算 CRC-16/MODBUS | ||
| 24 | + * | ||
| 25 | + * @param data 数据 | ||
| 26 | + * @param length 计算长度 | ||
| 27 | + * @return CRC16 值 | ||
| 28 | + */ | ||
| 29 | + public static int calculateCrc16(byte[] data, int length) { | ||
| 30 | + int crc = 0xFFFF; | ||
| 31 | + for (int i = 0; i < length; i++) { | ||
| 32 | + crc ^= (data[i] & 0xFF); | ||
| 33 | + for (int j = 0; j < 8; j++) { | ||
| 34 | + if ((crc & 0x0001) != 0) { | ||
| 35 | + crc >>= 1; | ||
| 36 | + crc ^= 0xA001; | ||
| 37 | + } else { | ||
| 38 | + crc >>= 1; | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | + return crc; | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 校验 CRC16 | ||
| 47 | + * | ||
| 48 | + * @param data 包含 CRC 的完整数据 | ||
| 49 | + * @return 校验是否通过 | ||
| 50 | + */ | ||
| 51 | + public static boolean verifyCrc16(byte[] data) { | ||
| 52 | + if (data.length < 3) { | ||
| 53 | + return false; | ||
| 54 | + } | ||
| 55 | + int computed = calculateCrc16(data, data.length - 2); | ||
| 56 | + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); | ||
| 57 | + return computed == received; | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + // ==================== 响应值提取 ==================== | ||
| 14 | 61 | ||
| 15 | /** | 62 | /** |
| 16 | * 从帧中提取寄存器值(FC01-04 读响应) | 63 | * 从帧中提取寄存器值(FC01-04 读响应) |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
| @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | ||
| 8 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; | 8 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; |
| 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; | 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; |
| 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; |
| 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| @@ -35,7 +35,7 @@ public class IotModbusTcpSlaveDownstreamHandler { | @@ -35,7 +35,7 @@ public class IotModbusTcpSlaveDownstreamHandler { | ||
| 35 | private final IotModbusTcpSlaveConnectionManager connectionManager; | 35 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 36 | private final IotModbusTcpSlaveConfigCacheService configCacheService; | 36 | private final IotModbusTcpSlaveConfigCacheService configCacheService; |
| 37 | private final IotModbusDataConverter dataConverter; | 37 | private final IotModbusDataConverter dataConverter; |
| 38 | - private final IotModbusFrameCodec frameCodec; | 38 | + private final IotModbusFrameEncoder frameEncoder; |
| 39 | 39 | ||
| 40 | /** | 40 | /** |
| 41 | * TCP 事务 ID 自增器 | 41 | * TCP 事务 ID 自增器 |
| @@ -117,11 +117,11 @@ public class IotModbusTcpSlaveDownstreamHandler { | @@ -117,11 +117,11 @@ public class IotModbusTcpSlaveDownstreamHandler { | ||
| 117 | } | 117 | } |
| 118 | if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) { | 118 | if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) { |
| 119 | // 单个值:使用单写功能码(FC05/FC06) | 119 | // 单个值:使用单写功能码(FC05/FC06) |
| 120 | - data = frameCodec.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), | 120 | + data = frameEncoder.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), |
| 121 | point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); | 121 | point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); |
| 122 | } else if (fcEnum.getWriteMultipleCode() != null) { | 122 | } else if (fcEnum.getWriteMultipleCode() != null) { |
| 123 | // 多个值:使用多写功能码(FC15/FC16) | 123 | // 多个值:使用多写功能码(FC15/FC16) |
| 124 | - data = frameCodec.encodeWriteMultipleRegistersRequest(slaveId, | 124 | + data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, |
| 125 | point.getRegisterAddress(), rawValues, frameFormat, transactionId); | 125 | point.getRegisterAddress(), rawValues, frameFormat, transactionId); |
| 126 | } else { | 126 | } else { |
| 127 | log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier()); | 127 | log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier()); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 3 | import cn.hutool.core.lang.Assert; | 4 | import cn.hutool.core.lang.Assert; |
| 4 | import cn.hutool.core.map.MapUtil; | 5 | import cn.hutool.core.map.MapUtil; |
| 6 | +import cn.hutool.core.util.BooleanUtil; | ||
| 5 | import cn.hutool.core.util.StrUtil; | 7 | import cn.hutool.core.util.StrUtil; |
| 6 | -import cn.hutool.extra.spring.SpringUtil; | ||
| 7 | import cn.hutool.json.JSONObject; | 8 | import cn.hutool.json.JSONObject; |
| 8 | import cn.hutool.json.JSONUtil; | 9 | import cn.hutool.json.JSONUtil; |
| 10 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 9 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 11 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 10 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 12 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 11 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 13 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| @@ -19,69 +21,76 @@ import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | @@ -19,69 +21,76 @@ import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | ||
| 19 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 21 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 20 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | 22 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; |
| 21 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; | 23 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; |
| 22 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; | ||
| 23 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusResponseParser; | 24 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; |
| 25 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils; | ||
| 24 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; | 26 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; |
| 25 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; | 27 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; |
| 26 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; | 28 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| 27 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; | 29 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; |
| 28 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; | 30 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; |
| 31 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; | ||
| 29 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 32 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 30 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 33 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 31 | import io.vertx.core.net.NetSocket; | 34 | import io.vertx.core.net.NetSocket; |
| 32 | -import lombok.Setter; | ||
| 33 | import lombok.extern.slf4j.Slf4j; | 35 | import lombok.extern.slf4j.Slf4j; |
| 34 | 36 | ||
| 35 | import java.util.Map; | 37 | import java.util.Map; |
| 36 | -import java.util.function.BiConsumer; | ||
| 37 | 38 | ||
| 38 | -// TODO @AI:逻辑有点多,看看是不是分区域! | 39 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; |
| 40 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | ||
| 41 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | ||
| 42 | + | ||
| 43 | +// DONE @AI:逻辑有点多,看看是不是分区域! => 已按区域划分:认证 / 轮询响应 | ||
| 39 | /** | 44 | /** |
| 40 | * IoT Modbus TCP Slave 上行数据处理器 | 45 | * IoT Modbus TCP Slave 上行数据处理器 |
| 41 | * <p> | 46 | * <p> |
| 42 | * 处理: | 47 | * 处理: |
| 43 | * 1. 自定义 FC 认证 | 48 | * 1. 自定义 FC 认证 |
| 44 | * 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post | 49 | * 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post |
| 45 | - * 3. 主动上报(mode=2)→ 透传 property.report TODO @AI:这种模式,应该不用支持;因为主动上报,都走标准的 tcp 即可; | 50 | + * // DONE @AI:不用主动上报;主动上报走标准 tcp 即可 |
| 46 | * | 51 | * |
| 47 | * @author 芋道源码 | 52 | * @author 芋道源码 |
| 48 | */ | 53 | */ |
| 49 | @Slf4j | 54 | @Slf4j |
| 50 | public class IotModbusTcpSlaveUpstreamHandler { | 55 | public class IotModbusTcpSlaveUpstreamHandler { |
| 51 | 56 | ||
| 57 | + private static final String METHOD_AUTH = "auth"; | ||
| 58 | + | ||
| 52 | private final IotDeviceCommonApi deviceApi; | 59 | private final IotDeviceCommonApi deviceApi; |
| 53 | private final IotDeviceMessageService messageService; | 60 | private final IotDeviceMessageService messageService; |
| 54 | private final IotModbusDataConverter dataConverter; | 61 | private final IotModbusDataConverter dataConverter; |
| 55 | - private final IotModbusFrameCodec frameCodec; | 62 | + private final IotModbusFrameEncoder frameEncoder; |
| 56 | private final IotModbusTcpSlaveConnectionManager connectionManager; | 63 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 57 | private final IotModbusTcpSlaveConfigCacheService configCacheService; | 64 | private final IotModbusTcpSlaveConfigCacheService configCacheService; |
| 58 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; | 65 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; |
| 66 | + private final IotModbusTcpSlavePollScheduler pollScheduler; | ||
| 67 | + private final IotDeviceService deviceService; | ||
| 59 | private final String serverId; | 68 | private final String serverId; |
| 60 | 69 | ||
| 61 | - /** | ||
| 62 | - * 认证成功回调:(deviceId, config) → 启动轮询等 | ||
| 63 | - */ | ||
| 64 | - @Setter | ||
| 65 | - private BiConsumer<Long, IotModbusDeviceConfigRespDTO> onAuthSuccess; | ||
| 66 | - | ||
| 67 | public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, | 70 | public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, |
| 68 | IotDeviceMessageService messageService, | 71 | IotDeviceMessageService messageService, |
| 69 | IotModbusDataConverter dataConverter, | 72 | IotModbusDataConverter dataConverter, |
| 70 | - IotModbusFrameCodec frameCodec, | 73 | + IotModbusFrameEncoder frameEncoder, |
| 71 | IotModbusTcpSlaveConnectionManager connectionManager, | 74 | IotModbusTcpSlaveConnectionManager connectionManager, |
| 72 | IotModbusTcpSlaveConfigCacheService configCacheService, | 75 | IotModbusTcpSlaveConfigCacheService configCacheService, |
| 73 | IotModbusTcpSlavePendingRequestManager pendingRequestManager, | 76 | IotModbusTcpSlavePendingRequestManager pendingRequestManager, |
| 77 | + IotModbusTcpSlavePollScheduler pollScheduler, | ||
| 78 | + IotDeviceService deviceService, | ||
| 74 | String serverId) { | 79 | String serverId) { |
| 75 | this.deviceApi = deviceApi; | 80 | this.deviceApi = deviceApi; |
| 76 | this.messageService = messageService; | 81 | this.messageService = messageService; |
| 77 | this.dataConverter = dataConverter; | 82 | this.dataConverter = dataConverter; |
| 78 | - this.frameCodec = frameCodec; | 83 | + this.frameEncoder = frameEncoder; |
| 79 | this.connectionManager = connectionManager; | 84 | this.connectionManager = connectionManager; |
| 80 | this.configCacheService = configCacheService; | 85 | this.configCacheService = configCacheService; |
| 81 | this.pendingRequestManager = pendingRequestManager; | 86 | this.pendingRequestManager = pendingRequestManager; |
| 87 | + this.pollScheduler = pollScheduler; | ||
| 88 | + this.deviceService = deviceService; | ||
| 82 | this.serverId = serverId; | 89 | this.serverId = serverId; |
| 83 | } | 90 | } |
| 84 | 91 | ||
| 92 | + // ========== 帧处理入口 ========== | ||
| 93 | + | ||
| 85 | /** | 94 | /** |
| 86 | * 处理帧 | 95 | * 处理帧 |
| 87 | */ | 96 | */ |
| @@ -89,159 +98,158 @@ public class IotModbusTcpSlaveUpstreamHandler { | @@ -89,159 +98,158 @@ public class IotModbusTcpSlaveUpstreamHandler { | ||
| 89 | if (frame == null) { | 98 | if (frame == null) { |
| 90 | return; | 99 | return; |
| 91 | } | 100 | } |
| 92 | - // 1.1 自定义功能码(认证等扩展) | ||
| 93 | - if (StrUtil.isNotEmpty(frame.getCustomData())) { | ||
| 94 | - handleCustomFrame(socket, frame, frameFormat); | ||
| 95 | - return; | ||
| 96 | - } | ||
| 97 | - // 1.2 异常响应 | 101 | + // 1. 异常响应 |
| 98 | if (frame.isException()) { | 102 | if (frame.isException()) { |
| 99 | - // TODO @AI:这种需要返回一个结果给 modbus client? | ||
| 100 | log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]", | 103 | log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]", |
| 101 | frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode()); | 104 | frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode()); |
| 102 | return; | 105 | return; |
| 103 | } | 106 | } |
| 104 | - // 1.3 未认证连接,丢弃 | 107 | + |
| 108 | + // 2. 自定义功能码(认证等扩展) | ||
| 109 | + if (StrUtil.isNotEmpty(frame.getCustomData())) { | ||
| 110 | + handleCustomFrame(socket, frame, frameFormat); | ||
| 111 | + return; | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + // 1.2 未认证连接,丢弃 | ||
| 115 | + // TODO @AI:把 1.2、1.3 拿到 handlePollingResponse 里;是否需要登录,自己知道! | ||
| 105 | if (!connectionManager.isAuthenticated(socket)) { | 116 | if (!connectionManager.isAuthenticated(socket)) { |
| 106 | - // TODO @AI:这种需要返回一个结果给 modbus client? | ||
| 107 | log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); | 117 | log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); |
| 108 | return; | 118 | return; |
| 109 | } | 119 | } |
| 110 | 120 | ||
| 111 | - // TODO @AI:获取不到,看看要不要也打个告警;然后 | ||
| 112 | - // 2. 标准 Modbus 响应 | 121 | + // 3. DONE @AI:断言必须是云端轮询(不再支持主动上报) |
| 122 | + // TODO @AI:貌似只能轮询到一次?! | ||
| 123 | + // 1.3 标准 Modbus 响应(只支持云端轮询模式) | ||
| 124 | + // TODO @AI:可以把 | ||
| 113 | ConnectionInfo info = connectionManager.getConnectionInfo(socket); | 125 | ConnectionInfo info = connectionManager.getConnectionInfo(socket); |
| 114 | if (info == null) { | 126 | if (info == null) { |
| 127 | + log.warn("[handleFrame][已认证但连接信息为空, remoteAddress={}]", socket.remoteAddress()); | ||
| 115 | return; | 128 | return; |
| 116 | } | 129 | } |
| 117 | - // TODO @AI:可以断言下,必须是云端轮询; | ||
| 118 | - if (info.getMode() != null && info.getMode().equals(IotModbusModeEnum.ACTIVE_REPORT.getMode())) { | ||
| 119 | - // mode=2:主动上报,透传 | ||
| 120 | - handleActiveReport(info, frame); | ||
| 121 | - } else { | ||
| 122 | - // mode=1:云端轮询,匹配 PendingRequest | ||
| 123 | - handlePollingResponse(info, frame, frameFormat); | ||
| 124 | - } | 130 | + handlePollingResponse(info, frame, frameFormat); |
| 125 | } | 131 | } |
| 126 | 132 | ||
| 133 | + // ========== 自定义 FC 处理(认证等) ========== | ||
| 134 | + | ||
| 127 | /** | 135 | /** |
| 128 | * 处理自定义功能码帧 | 136 | * 处理自定义功能码帧 |
| 137 | + * <p> | ||
| 138 | + * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler} | ||
| 129 | */ | 139 | */ |
| 130 | private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { | 140 | private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { |
| 131 | try { | 141 | try { |
| 132 | - // TODO @AI:直接使用 JsonUtils 去解析出 IotDeviceMessage | ||
| 133 | JSONObject json = JSONUtil.parseObj(frame.getCustomData()); | 142 | JSONObject json = JSONUtil.parseObj(frame.getCustomData()); |
| 134 | String method = json.getStr("method"); | 143 | String method = json.getStr("method"); |
| 135 | - // TODO @AI: method 枚举下; | ||
| 136 | - if ("auth".equals(method)) { | 144 | + if (METHOD_AUTH.equals(method)) { |
| 137 | handleAuth(socket, frame, json, frameFormat); | 145 | handleAuth(socket, frame, json, frameFormat); |
| 138 | return; | 146 | return; |
| 139 | } | 147 | } |
| 140 | - // TODO @AI:把 frame 都打印下; | ||
| 141 | - log.warn("[handleCustomFrame][未知 method: {}]", method); | 148 | + log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]", |
| 149 | + method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData()); | ||
| 150 | + } catch (ServiceException e) { | ||
| 151 | + // 已知业务异常,返回对应的错误码和错误信息 | ||
| 152 | + sendCustomResponse(socket, frame, frameFormat, e.getCode(), e.getMessage()); | ||
| 153 | + } catch (IllegalArgumentException e) { | ||
| 154 | + // 参数校验异常,返回 400 错误 | ||
| 155 | + sendCustomResponse(socket, frame, frameFormat, BAD_REQUEST.getCode(), e.getMessage()); | ||
| 142 | } catch (Exception e) { | 156 | } catch (Exception e) { |
| 143 | - // TODO @AI:各种情况的翻译;看看怎么弄比较合适;是不是要用 fc 自定义的 callback 下? | ||
| 144 | - log.error("[handleCustomFrame][解析自定义 FC 数据失败]", e); | 157 | + // 其他未知异常,返回 500 错误 |
| 158 | + log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]", | ||
| 159 | + frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e); | ||
| 160 | + sendCustomResponse(socket, frame, frameFormat, | ||
| 161 | + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | ||
| 145 | } | 162 | } |
| 146 | } | 163 | } |
| 147 | 164 | ||
| 165 | + // TODO @芋艿:在 review 下这个类; | ||
| 166 | + // TODO @AI:不传递 json,直接在 frame | ||
| 148 | /** | 167 | /** |
| 149 | * 处理认证请求 | 168 | * 处理认证请求 |
| 150 | */ | 169 | */ |
| 151 | private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json, | 170 | private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json, |
| 152 | IotModbusFrameFormatEnum frameFormat) { | 171 | IotModbusFrameFormatEnum frameFormat) { |
| 153 | - // TODO @AI:参数为空的校验; | 172 | + // TODO @AI:是不是可以 JsonUtils.convert(json, IotDeviceAuthReqDTO.class); |
| 154 | JSONObject params = json.getJSONObject("params"); | 173 | JSONObject params = json.getJSONObject("params"); |
| 155 | if (params == null) { | 174 | if (params == null) { |
| 156 | - sendAuthResponse(socket, frame, frameFormat, 1, "params 为空"); | ||
| 157 | - return; | 175 | + throw invalidParamException("params 不能为空"); |
| 158 | } | 176 | } |
| 159 | - // TODO @AI:参数判空; | 177 | + // DONE @AI:参数判空 |
| 160 | String clientId = params.getStr("clientId"); | 178 | String clientId = params.getStr("clientId"); |
| 161 | String username = params.getStr("username"); | 179 | String username = params.getStr("username"); |
| 162 | String password = params.getStr("password"); | 180 | String password = params.getStr("password"); |
| 181 | + // TODO @AI:逐个判空; | ||
| 182 | + if (StrUtil.hasBlank(clientId, username, password)) { | ||
| 183 | + throw invalidParamException("clientId、username、password 不能为空"); | ||
| 184 | + } | ||
| 163 | 185 | ||
| 164 | - try { | ||
| 165 | - // 1. 调用认证 API | ||
| 166 | - IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO() | ||
| 167 | - .setClientId(clientId).setUsername(username).setPassword(password); | ||
| 168 | - CommonResult<Boolean> authResult = deviceApi.authDevice(authReq); | ||
| 169 | - // TODO @AI:应该不用 close 吧?! | ||
| 170 | - // TODO @AI:BooleanUtils.isFalse | ||
| 171 | - if (authResult == null || !authResult.isSuccess() || !Boolean.TRUE.equals(authResult.getData())) { | ||
| 172 | - log.warn("[handleAuth][认证失败, clientId={}, username={}]", clientId, username); | ||
| 173 | - sendAuthResponse(socket, frame, frameFormat, 1, "认证失败"); | ||
| 174 | - socket.close(); | ||
| 175 | - return; | ||
| 176 | - } | ||
| 177 | - | ||
| 178 | - // 2. 认证成功,查找设备配置(通过 username 作为 deviceName 查找) | ||
| 179 | - // TODO 根据实际的认证模型优化查找逻辑 | ||
| 180 | - // TODO @AI:通过 device | ||
| 181 | - IotModbusDeviceConfigRespDTO config = configCacheService.findConfigByAuth(clientId, username, password); | ||
| 182 | - if (config == null) { | ||
| 183 | - // 退而求其次,遍历缓存查找 | ||
| 184 | - log.info("[handleAuth][认证成功但未找到设备配置, clientId={}, username={}]", clientId, username); | ||
| 185 | - } | ||
| 186 | - // 2.2 解析设备信息 | ||
| 187 | - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | ||
| 188 | - Assert.notNull(deviceInfo, "解析设备信息失败"); | ||
| 189 | - // 2.3 获取设备信息 | ||
| 190 | - // TODO @AI:这里要优化下,不要通过 spring 这样注入; | ||
| 191 | - IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); | ||
| 192 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | ||
| 193 | - Assert.notNull(device, "设备不存在"); | ||
| 194 | - // TODO @AI:校验 frameFormat 是否一致;不一致,连接也失败; | 186 | + // 1. 调用认证 API |
| 187 | + IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO() | ||
| 188 | + .setClientId(clientId).setUsername(username).setPassword(password); | ||
| 189 | + CommonResult<Boolean> authResult = deviceApi.authDevice(authReq); | ||
| 190 | + authResult.checkError(); | ||
| 191 | + if (BooleanUtil.isFalse(authResult.getData())) { | ||
| 192 | + log.warn("[handleAuth][认证失败, clientId={}, username={}]", clientId, username); | ||
| 193 | + sendCustomResponse(socket, frame, frameFormat, 1, "认证失败"); | ||
| 194 | + return; | ||
| 195 | + } | ||
| 195 | 196 | ||
| 196 | - // 3. 注册连接 | ||
| 197 | - ConnectionInfo connectionInfo = new ConnectionInfo() | ||
| 198 | - .setDeviceId(device.getId()) | ||
| 199 | - .setSlaveId(frame.getSlaveId()) | ||
| 200 | - .setFrameFormat(frameFormat) | ||
| 201 | - .setMode(config != null ? config.getMode() : IotModbusModeEnum.POLLING.getMode()); | 197 | + // 2.1 认证成功,查找设备配置 |
| 198 | + IotModbusDeviceConfigRespDTO config = configCacheService.findConfigByAuth(clientId, username, password); | ||
| 199 | + if (config == null) { | ||
| 200 | + log.info("[handleAuth][认证成功但未找到设备配置, clientId={}, username={}]", clientId, username); | ||
| 201 | + } | ||
| 202 | + // 2.2 解析设备信息 | ||
| 203 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | ||
| 204 | + Assert.notNull(deviceInfo, "解析设备信息失败"); | ||
| 205 | + // 2.3 获取设备信息 | ||
| 206 | + // DONE @AI:IotDeviceService 作为构造参数传入,不通过 SpringUtil.getBean | ||
| 207 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | ||
| 208 | + Assert.notNull(device, "设备不存在"); | ||
| 202 | 209 | ||
| 203 | - if (config != null) { | ||
| 204 | - connectionInfo.setDeviceId(config.getDeviceId()) | ||
| 205 | - .setProductKey(config.getProductKey()) | ||
| 206 | - .setDeviceName(config.getDeviceName()); | ||
| 207 | - } | ||
| 208 | - connectionManager.registerConnection(socket, connectionInfo); | 210 | + // 3. 注册连接 |
| 211 | + ConnectionInfo connectionInfo = new ConnectionInfo() | ||
| 212 | + .setDeviceId(device.getId()) | ||
| 213 | + .setSlaveId(frame.getSlaveId()) | ||
| 214 | + .setFrameFormat(frameFormat) | ||
| 215 | + .setMode(config != null ? config.getMode() : IotModbusModeEnum.POLLING.getMode()); | ||
| 216 | + if (config != null) { | ||
| 217 | + connectionInfo.setDeviceId(config.getDeviceId()) | ||
| 218 | + .setProductKey(config.getProductKey()) | ||
| 219 | + .setDeviceName(config.getDeviceName()); | ||
| 220 | + } | ||
| 221 | + connectionManager.registerConnection(socket, connectionInfo); | ||
| 209 | 222 | ||
| 210 | - // 4. 发送认证成功响应 | ||
| 211 | - sendAuthResponse(socket, frame, frameFormat, 0, "success"); | ||
| 212 | - log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId, | ||
| 213 | - config != null ? config.getDeviceId() : "unknown"); | 223 | + // 4. 发送认证成功响应 |
| 224 | + sendCustomResponse(socket, frame, frameFormat, 0, "success"); | ||
| 225 | + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId, | ||
| 226 | + config != null ? config.getDeviceId() : device.getId()); | ||
| 214 | 227 | ||
| 215 | - // 5. 回调:启动轮询等 | ||
| 216 | - // TODO @AI:是不是不要 callback,而是主动调用! | ||
| 217 | - if (onAuthSuccess != null && config != null) { | ||
| 218 | - onAuthSuccess.accept(config.getDeviceId(), config); | ||
| 219 | - } | ||
| 220 | - } catch (Exception e) { | ||
| 221 | - log.error("[handleAuth][认证异常]", e); | ||
| 222 | - sendAuthResponse(socket, frame, frameFormat, 1, "认证异常"); | ||
| 223 | - socket.close(); | 228 | + // 5. 直接启动轮询 |
| 229 | + if (config != null) { | ||
| 230 | + pollScheduler.updatePolling(config); | ||
| 224 | } | 231 | } |
| 225 | } | 232 | } |
| 226 | 233 | ||
| 227 | /** | 234 | /** |
| 228 | - * 发送认证响应 | 235 | + * 发送自定义功能码响应 |
| 229 | */ | 236 | */ |
| 230 | - private void sendAuthResponse(NetSocket socket, IotModbusFrame frame, | ||
| 231 | - IotModbusFrameFormatEnum frameFormat, | ||
| 232 | - int code, String message) { | ||
| 233 | - // TODO @AI:不一定用 auth response;而是 custom? | 237 | + private void sendCustomResponse(NetSocket socket, IotModbusFrame frame, |
| 238 | + IotModbusFrameFormatEnum frameFormat, | ||
| 239 | + int code, String message) { | ||
| 234 | JSONObject resp = new JSONObject(); | 240 | JSONObject resp = new JSONObject(); |
| 235 | - resp.set("method", "auth"); | 241 | + resp.set("method", METHOD_AUTH); |
| 236 | resp.set("code", code); | 242 | resp.set("code", code); |
| 237 | resp.set("message", message); | 243 | resp.set("message", message); |
| 238 | - byte[] data = frameCodec.encodeCustomFrame(frame.getSlaveId(), resp.toString(), | 244 | + byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), resp.toString(), |
| 239 | frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); | 245 | frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); |
| 240 | connectionManager.sendToSocket(socket, data); | 246 | connectionManager.sendToSocket(socket, data); |
| 241 | } | 247 | } |
| 242 | 248 | ||
| 249 | + // ========== 轮询响应处理 ========== | ||
| 250 | + | ||
| 243 | /** | 251 | /** |
| 244 | - * 处理轮询响应(mode=1) | 252 | + * 处理轮询响应(云端轮询模式) |
| 245 | */ | 253 | */ |
| 246 | private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame, | 254 | private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame, |
| 247 | IotModbusFrameFormatEnum frameFormat) { | 255 | IotModbusFrameFormatEnum frameFormat) { |
| @@ -254,7 +262,7 @@ public class IotModbusTcpSlaveUpstreamHandler { | @@ -254,7 +262,7 @@ public class IotModbusTcpSlaveUpstreamHandler { | ||
| 254 | return; | 262 | return; |
| 255 | } | 263 | } |
| 256 | // 1.2 提取寄存器值 | 264 | // 1.2 提取寄存器值 |
| 257 | - int[] rawValues = IotModbusResponseParser.extractValues(frame); | 265 | + int[] rawValues = IotModbusUtils.extractValues(frame); |
| 258 | if (rawValues == null) { | 266 | if (rawValues == null) { |
| 259 | log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", | 267 | log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", |
| 260 | info.getDeviceId(), request.getIdentifier()); | 268 | info.getDeviceId(), request.getIdentifier()); |
| @@ -262,20 +270,17 @@ public class IotModbusTcpSlaveUpstreamHandler { | @@ -262,20 +270,17 @@ public class IotModbusTcpSlaveUpstreamHandler { | ||
| 262 | } | 270 | } |
| 263 | // 1.3 查找点位配置 | 271 | // 1.3 查找点位配置 |
| 264 | IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); | 272 | IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); |
| 265 | - if (config == null || config.getPoints() == null) { | 273 | + if (config == null || CollUtil.isEmpty(config.getPoints())) { |
| 266 | return; | 274 | return; |
| 267 | } | 275 | } |
| 268 | - // TODO @AI:findone arrayUtil; | ||
| 269 | - var point = config.getPoints().stream() | ||
| 270 | - .filter(p -> p.getId().equals(request.getPointId())) | ||
| 271 | - .findFirst().orElse(null); | 276 | + var point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(request.getPointId())); |
| 272 | if (point == null) { | 277 | if (point == null) { |
| 273 | return; | 278 | return; |
| 274 | } | 279 | } |
| 275 | 280 | ||
| 276 | - // TODO @AI:拆成 2.1、2.2 | ||
| 277 | - // 4. 点位翻译 → 上报 | 281 | + // 2.1 点位翻译 |
| 278 | Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); | 282 | Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); |
| 283 | + // 2.2 上报属性 | ||
| 279 | Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue); | 284 | Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue); |
| 280 | IotDeviceMessage message = IotDeviceMessage.requestOf( | 285 | IotDeviceMessage message = IotDeviceMessage.requestOf( |
| 281 | IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); | 286 | IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); |
| @@ -284,29 +289,4 @@ public class IotModbusTcpSlaveUpstreamHandler { | @@ -284,29 +289,4 @@ public class IotModbusTcpSlaveUpstreamHandler { | ||
| 284 | info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue); | 289 | info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue); |
| 285 | } | 290 | } |
| 286 | 291 | ||
| 287 | - // TODO @AI:不需要这个逻辑; | ||
| 288 | - /** | ||
| 289 | - * 处理主动上报(mode=2) | ||
| 290 | - * 设备直接上报 property.report 格式:{propertyId: value},不做点位翻译 | ||
| 291 | - */ | ||
| 292 | - @SuppressWarnings("unchecked") | ||
| 293 | - private void handleActiveReport(ConnectionInfo info, IotModbusFrame frame) { | ||
| 294 | - // mode=2 下设备上报标准 Modbus 帧,但由于没有点位翻译, | ||
| 295 | - // 这里暂时将原始寄存器值以 FC+地址 为 key 上报 | ||
| 296 | - int[] rawValues = IotModbusResponseParser.extractValues(frame); | ||
| 297 | - if (rawValues == null) { | ||
| 298 | - return; | ||
| 299 | - } | ||
| 300 | - | ||
| 301 | - // 简单上报:以 "register_FC{fc}" 作为属性名 | ||
| 302 | - String propertyKey = "register_FC" + frame.getFunctionCode(); | ||
| 303 | - Map<String, Object> params = MapUtil.of(propertyKey, rawValues); | ||
| 304 | - IotDeviceMessage message = IotDeviceMessage.requestOf( | ||
| 305 | - IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); | ||
| 306 | - messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId); | ||
| 307 | - | ||
| 308 | - log.debug("[handleActiveReport][设备={}, FC={}, 原始值={}]", | ||
| 309 | - info.getDeviceId(), frame.getFunctionCode(), rawValues); | ||
| 310 | - } | ||
| 311 | - | ||
| 312 | } | 292 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java
| @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; | @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; | 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; |
| 10 | import io.vertx.core.Vertx; | 10 | import io.vertx.core.Vertx; |
| @@ -37,7 +37,7 @@ public class IotModbusTcpSlavePollScheduler { | @@ -37,7 +37,7 @@ public class IotModbusTcpSlavePollScheduler { | ||
| 37 | 37 | ||
| 38 | private final Vertx vertx; | 38 | private final Vertx vertx; |
| 39 | private final IotModbusTcpSlaveConnectionManager connectionManager; | 39 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 40 | - private final IotModbusFrameCodec frameCodec; | 40 | + private final IotModbusFrameEncoder frameEncoder; |
| 41 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; | 41 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; |
| 42 | private final int requestTimeout; | 42 | private final int requestTimeout; |
| 43 | 43 | ||
| @@ -163,7 +163,7 @@ public class IotModbusTcpSlavePollScheduler { | @@ -163,7 +163,7 @@ public class IotModbusTcpSlavePollScheduler { | ||
| 163 | int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; | 163 | int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; |
| 164 | int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; | 164 | int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; |
| 165 | // 2.2 编码读请求 | 165 | // 2.2 编码读请求 |
| 166 | - byte[] data = frameCodec.encodeReadRequest(slaveId, point.getFunctionCode(), | 166 | + byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), |
| 167 | point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); | 167 | point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); |
| 168 | // 2.3 注册 PendingRequest | 168 | // 2.3 注册 PendingRequest |
| 169 | PendingRequest pendingRequest = new PendingRequest( | 169 | PendingRequest pendingRequest = new PendingRequest( |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java
| @@ -6,8 +6,9 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | @@ -6,8 +6,9 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; | ||
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory; | 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; | ||
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils; | ||
| 11 | import io.vertx.core.Vertx; | 12 | import io.vertx.core.Vertx; |
| 12 | import io.vertx.core.buffer.Buffer; | 13 | import io.vertx.core.buffer.Buffer; |
| 13 | import io.vertx.core.net.NetClient; | 14 | import io.vertx.core.net.NetClient; |
| @@ -59,7 +60,8 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | @@ -59,7 +60,8 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | ||
| 59 | 60 | ||
| 60 | // ===================== 编解码器 ===================== | 61 | // ===================== 编解码器 ===================== |
| 61 | 62 | ||
| 62 | - private static final IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC); | 63 | + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC); |
| 64 | + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC); | ||
| 63 | 65 | ||
| 64 | // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== | 66 | // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== |
| 65 | 67 | ||
| @@ -111,7 +113,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | @@ -111,7 +113,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | ||
| 111 | // ===================== 轮询响应测试 ===================== | 113 | // ===================== 轮询响应测试 ===================== |
| 112 | 114 | ||
| 113 | /** | 115 | /** |
| 114 | - * 轮询响应测试:认证后等待网关下发 FC03 读请求(RTU 格式),构造读响应帧发回 | 116 | + * 轮询响应测试:认证后持续监听网关下发的读请求(RTU 格式),每次收到都自动构造读响应帧发回 |
| 115 | */ | 117 | */ |
| 116 | @Test | 118 | @Test |
| 117 | public void testPollingResponse() throws Exception { | 119 | public void testPollingResponse() throws Exception { |
| @@ -121,30 +123,32 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | @@ -121,30 +123,32 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | ||
| 121 | IotModbusFrame authResponse = authenticate(socket); | 123 | IotModbusFrame authResponse = authenticate(socket); |
| 122 | log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); | 124 | log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); |
| 123 | 125 | ||
| 124 | - // 2. 等待网关下发读请求 | ||
| 125 | - log.info("[testPollingResponse][等待网关下发读请求...]"); | ||
| 126 | - IotModbusFrame readRequest = waitForRequest(socket); | ||
| 127 | - log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}]", | ||
| 128 | - readRequest.getSlaveId(), readRequest.getFunctionCode()); | ||
| 129 | - | ||
| 130 | - // 3. 解析读请求中的起始地址和数量 | ||
| 131 | - byte[] pdu = readRequest.getPdu(); | ||
| 132 | - int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); | ||
| 133 | - int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); | ||
| 134 | - log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); | ||
| 135 | - | ||
| 136 | - // 4. 构造读响应帧(模拟寄存器数据,RTU 格式) | ||
| 137 | - int[] registerValues = new int[quantity]; | ||
| 138 | - for (int i = 0; i < quantity; i++) { | ||
| 139 | - registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... | ||
| 140 | - } | ||
| 141 | - byte[] responseData = buildReadResponse(readRequest.getSlaveId(), | ||
| 142 | - readRequest.getFunctionCode(), registerValues); | ||
| 143 | - socket.write(Buffer.buffer(responseData)); | ||
| 144 | - log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); | ||
| 145 | - | ||
| 146 | - // 5. 等待一段时间让网关处理 | ||
| 147 | - Thread.sleep(20000); | 126 | + // 2. 设置持续监听:每收到一个读请求,自动回复 |
| 127 | + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); | ||
| 128 | + CompletableFuture<Void> done = new CompletableFuture<>(); | ||
| 129 | + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { | ||
| 130 | + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]", | ||
| 131 | + frame.getSlaveId(), frame.getFunctionCode()); | ||
| 132 | + // 解析读请求中的起始地址和数量 | ||
| 133 | + byte[] pdu = frame.getPdu(); | ||
| 134 | + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); | ||
| 135 | + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); | ||
| 136 | + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); | ||
| 137 | + | ||
| 138 | + // 构造读响应帧(模拟寄存器数据,RTU 格式) | ||
| 139 | + int[] registerValues = new int[quantity]; | ||
| 140 | + for (int i = 0; i < quantity; i++) { | ||
| 141 | + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... | ||
| 142 | + } | ||
| 143 | + byte[] responseData = buildReadResponse(frame.getSlaveId(), | ||
| 144 | + frame.getFunctionCode(), registerValues); | ||
| 145 | + socket.write(Buffer.buffer(responseData)); | ||
| 146 | + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); | ||
| 147 | + }); | ||
| 148 | + socket.handler(parser); | ||
| 149 | + | ||
| 150 | + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 | ||
| 151 | + Thread.sleep(200000); | ||
| 148 | } finally { | 152 | } finally { |
| 149 | socket.close(); | 153 | socket.close(); |
| 150 | } | 154 | } |
| @@ -199,23 +203,20 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | @@ -199,23 +203,20 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | ||
| 199 | } | 203 | } |
| 200 | 204 | ||
| 201 | /** | 205 | /** |
| 202 | - * 发送帧并等待响应(使用 IotModbusRecordParserFactory 自动检测帧格式) | 206 | + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码) |
| 203 | */ | 207 | */ |
| 204 | private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { | 208 | private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { |
| 205 | CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>(); | 209 | CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>(); |
| 206 | - // 使用 RecordParserFactory 创建拆包器(自动检测帧格式) | ||
| 207 | - RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC, | ||
| 208 | - buffer -> { | 210 | + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame) |
| 211 | + RecordParser parser = FRAME_DECODER.createRecordParser( | ||
| 212 | + (frame, frameFormat) -> { | ||
| 209 | try { | 213 | try { |
| 210 | - // 检测到的帧格式应该是 RTU,使用 RTU 格式解码 | ||
| 211 | - IotModbusFrame frame = FRAME_CODEC.decodeResponse( | ||
| 212 | - buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); | 214 | + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat); |
| 213 | responseFuture.complete(frame); | 215 | responseFuture.complete(frame); |
| 214 | } catch (Exception e) { | 216 | } catch (Exception e) { |
| 215 | responseFuture.completeExceptionally(e); | 217 | responseFuture.completeExceptionally(e); |
| 216 | } | 218 | } |
| 217 | - }, | ||
| 218 | - format -> log.info("[sendAndReceive][检测到帧格式: {}]", format)); | 219 | + }); |
| 219 | socket.handler(parser); | 220 | socket.handler(parser); |
| 220 | 221 | ||
| 221 | // 发送请求 | 222 | // 发送请求 |
| @@ -231,18 +232,16 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | @@ -231,18 +232,16 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | ||
| 231 | */ | 232 | */ |
| 232 | private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { | 233 | private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { |
| 233 | CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>(); | 234 | CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>(); |
| 234 | - // 使用 RecordParserFactory 创建拆包器 | ||
| 235 | - RecordParser parser = IotModbusRecordParserFactory.create(CUSTOM_FC, | ||
| 236 | - buffer -> { | 235 | + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame) |
| 236 | + RecordParser parser = FRAME_DECODER.createRecordParser( | ||
| 237 | + (frame, frameFormat) -> { | ||
| 237 | try { | 238 | try { |
| 238 | - IotModbusFrame frame = FRAME_CODEC.decodeResponse( | ||
| 239 | - buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); | 239 | + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat); |
| 240 | requestFuture.complete(frame); | 240 | requestFuture.complete(frame); |
| 241 | } catch (Exception e) { | 241 | } catch (Exception e) { |
| 242 | requestFuture.completeExceptionally(e); | 242 | requestFuture.completeExceptionally(e); |
| 243 | } | 243 | } |
| 244 | - }, | ||
| 245 | - format -> log.info("[waitForRequest][检测到帧格式: {}]", format)); | 244 | + }); |
| 246 | socket.handler(parser); | 245 | socket.handler(parser); |
| 247 | 246 | ||
| 248 | // 等待(超时 30 秒,因为轮询间隔可能比较长) | 247 | // 等待(超时 30 秒,因为轮询间隔可能比较长) |
| @@ -264,7 +263,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | @@ -264,7 +263,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | ||
| 264 | JSONObject json = new JSONObject(); | 263 | JSONObject json = new JSONObject(); |
| 265 | json.set("method", "auth"); | 264 | json.set("method", "auth"); |
| 266 | json.set("params", params); | 265 | json.set("params", params); |
| 267 | - return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(), | 266 | + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), |
| 268 | IotModbusFrameFormatEnum.MODBUS_RTU, 0); | 267 | IotModbusFrameFormatEnum.MODBUS_RTU, 0); |
| 269 | } | 268 | } |
| 270 | 269 | ||
| @@ -286,7 +285,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | @@ -286,7 +285,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { | ||
| 286 | frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF); | 285 | frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF); |
| 287 | } | 286 | } |
| 288 | // 计算 CRC16 | 287 | // 计算 CRC16 |
| 289 | - int crc = IotModbusFrameCodec.calculateCrc16(frame, totalLength - 2); | 288 | + int crc = IotModbusUtils.calculateCrc16(frame, totalLength - 2); |
| 290 | frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low | 289 | frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low |
| 291 | frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High | 290 | frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High |
| 292 | return frame; | 291 | return frame; |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java
| @@ -6,8 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | @@ -6,8 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; | ||
| 10 | -import io.vertx.core.Handler; | 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; |
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; | ||
| 11 | import io.vertx.core.Vertx; | 11 | import io.vertx.core.Vertx; |
| 12 | import io.vertx.core.buffer.Buffer; | 12 | import io.vertx.core.buffer.Buffer; |
| 13 | import io.vertx.core.net.NetClient; | 13 | import io.vertx.core.net.NetClient; |
| @@ -61,7 +61,8 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | @@ -61,7 +61,8 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | ||
| 61 | 61 | ||
| 62 | // ===================== 编解码器 ===================== | 62 | // ===================== 编解码器 ===================== |
| 63 | 63 | ||
| 64 | - private static final IotModbusFrameCodec FRAME_CODEC = new IotModbusFrameCodec(CUSTOM_FC); | 64 | + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC); |
| 65 | + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC); | ||
| 65 | 66 | ||
| 66 | // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== | 67 | // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== |
| 67 | 68 | ||
| @@ -113,7 +114,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | @@ -113,7 +114,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | ||
| 113 | // ===================== 轮询响应测试 ===================== | 114 | // ===================== 轮询响应测试 ===================== |
| 114 | 115 | ||
| 115 | /** | 116 | /** |
| 116 | - * 轮询响应测试:认证后等待网关下发 FC03 读请求,构造读响应帧发回 | 117 | + * 轮询响应测试:认证后持续监听网关下发的读请求,每次收到都自动构造读响应帧发回 |
| 117 | */ | 118 | */ |
| 118 | @Test | 119 | @Test |
| 119 | public void testPollingResponse() throws Exception { | 120 | public void testPollingResponse() throws Exception { |
| @@ -123,29 +124,31 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | @@ -123,29 +124,31 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | ||
| 123 | IotModbusFrame authResponse = authenticate(socket); | 124 | IotModbusFrame authResponse = authenticate(socket); |
| 124 | log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); | 125 | log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); |
| 125 | 126 | ||
| 126 | - // 2. 等待网关下发读请求 | ||
| 127 | - log.info("[testPollingResponse][等待网关下发读请求...]"); | ||
| 128 | - IotModbusFrame readRequest = waitForRequest(socket); | ||
| 129 | - log.info("[testPollingResponse][收到读请求: slaveId={}, FC={}, transactionId={}]", | ||
| 130 | - readRequest.getSlaveId(), readRequest.getFunctionCode(), readRequest.getTransactionId()); | ||
| 131 | - | ||
| 132 | - // 3. 解析读请求中的起始地址和数量 | ||
| 133 | - byte[] pdu = readRequest.getPdu(); | ||
| 134 | - int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); | ||
| 135 | - int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); | ||
| 136 | - log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); | ||
| 137 | - | ||
| 138 | - // 4. 构造读响应帧(模拟寄存器数据) | ||
| 139 | - int[] registerValues = new int[quantity]; | ||
| 140 | - for (int i = 0; i < quantity; i++) { | ||
| 141 | - registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... | ||
| 142 | - } | ||
| 143 | - byte[] responseData = buildReadResponse(readRequest.getTransactionId(), | ||
| 144 | - readRequest.getSlaveId(), readRequest.getFunctionCode(), registerValues); | ||
| 145 | - socket.write(Buffer.buffer(responseData)); | ||
| 146 | - log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); | ||
| 147 | - | ||
| 148 | - // 5. 等待一段时间让网关处理 | 127 | + // 2. 设置持续监听:每收到一个读请求,自动回复 |
| 128 | + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); | ||
| 129 | + CompletableFuture<Void> done = new CompletableFuture<>(); | ||
| 130 | + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> { | ||
| 131 | + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}, transactionId={}]", | ||
| 132 | + frame.getSlaveId(), frame.getFunctionCode(), frame.getTransactionId()); | ||
| 133 | + // 解析读请求中的起始地址和数量 | ||
| 134 | + byte[] pdu = frame.getPdu(); | ||
| 135 | + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF); | ||
| 136 | + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF); | ||
| 137 | + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity); | ||
| 138 | + | ||
| 139 | + // 构造读响应帧(模拟寄存器数据) | ||
| 140 | + int[] registerValues = new int[quantity]; | ||
| 141 | + for (int i = 0; i < quantity; i++) { | ||
| 142 | + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ... | ||
| 143 | + } | ||
| 144 | + byte[] responseData = buildReadResponse(frame.getTransactionId(), | ||
| 145 | + frame.getSlaveId(), frame.getFunctionCode(), registerValues); | ||
| 146 | + socket.write(Buffer.buffer(responseData)); | ||
| 147 | + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues); | ||
| 148 | + }); | ||
| 149 | + socket.handler(parser); | ||
| 150 | + | ||
| 151 | + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求 | ||
| 149 | Thread.sleep(200000); | 152 | Thread.sleep(200000); |
| 150 | } finally { | 153 | } finally { |
| 151 | socket.close(); | 154 | socket.close(); |
| @@ -201,15 +204,20 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | @@ -201,15 +204,20 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | ||
| 201 | } | 204 | } |
| 202 | 205 | ||
| 203 | /** | 206 | /** |
| 204 | - * 发送帧并等待响应(MODBUS_TCP 格式) | ||
| 205 | - * <p> | ||
| 206 | - * 使用两阶段 RecordParser 拆包:fixedSizeMode(6) 读 MBAP 头 → fixedSizeMode(length) 读 body | 207 | + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码) |
| 207 | */ | 208 | */ |
| 208 | private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { | 209 | private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { |
| 209 | CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>(); | 210 | CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>(); |
| 210 | - // 创建 TCP 两阶段拆包 RecordParser | ||
| 211 | - RecordParser parser = RecordParser.newFixed(6); | ||
| 212 | - parser.handler(new TcpRecordParserHandler(parser, responseFuture)); | 211 | + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame) |
| 212 | + RecordParser parser = FRAME_DECODER.createRecordParser( | ||
| 213 | + (frame, frameFormat) -> { | ||
| 214 | + try { | ||
| 215 | + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat); | ||
| 216 | + responseFuture.complete(frame); | ||
| 217 | + } catch (Exception e) { | ||
| 218 | + responseFuture.completeExceptionally(e); | ||
| 219 | + } | ||
| 220 | + }); | ||
| 213 | socket.handler(parser); | 221 | socket.handler(parser); |
| 214 | 222 | ||
| 215 | // 发送请求 | 223 | // 发送请求 |
| @@ -225,8 +233,16 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | @@ -225,8 +233,16 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | ||
| 225 | */ | 233 | */ |
| 226 | private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { | 234 | private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { |
| 227 | CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>(); | 235 | CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>(); |
| 228 | - RecordParser parser = RecordParser.newFixed(6); | ||
| 229 | - parser.handler(new TcpRecordParserHandler(parser, requestFuture)); | 236 | + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame) |
| 237 | + RecordParser parser = FRAME_DECODER.createRecordParser( | ||
| 238 | + (frame, frameFormat) -> { | ||
| 239 | + try { | ||
| 240 | + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat); | ||
| 241 | + requestFuture.complete(frame); | ||
| 242 | + } catch (Exception e) { | ||
| 243 | + requestFuture.completeExceptionally(e); | ||
| 244 | + } | ||
| 245 | + }); | ||
| 230 | socket.handler(parser); | 246 | socket.handler(parser); |
| 231 | 247 | ||
| 232 | // 等待(超时 30 秒,因为轮询间隔可能比较长) | 248 | // 等待(超时 30 秒,因为轮询间隔可能比较长) |
| @@ -234,46 +250,6 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | @@ -234,46 +250,6 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | ||
| 234 | } | 250 | } |
| 235 | 251 | ||
| 236 | /** | 252 | /** |
| 237 | - * MODBUS_TCP 两阶段拆包 Handler | ||
| 238 | - */ | ||
| 239 | - private class TcpRecordParserHandler implements Handler<Buffer> { | ||
| 240 | - | ||
| 241 | - private final RecordParser parser; | ||
| 242 | - private final CompletableFuture<IotModbusFrame> future; | ||
| 243 | - private byte[] mbapHeader; | ||
| 244 | - private boolean waitingForBody = false; | ||
| 245 | - | ||
| 246 | - TcpRecordParserHandler(RecordParser parser, CompletableFuture<IotModbusFrame> future) { | ||
| 247 | - this.parser = parser; | ||
| 248 | - this.future = future; | ||
| 249 | - } | ||
| 250 | - | ||
| 251 | - @Override | ||
| 252 | - public void handle(Buffer buffer) { | ||
| 253 | - try { | ||
| 254 | - if (waitingForBody) { | ||
| 255 | - // Phase 2: 收到 body(unitId + PDU) | ||
| 256 | - byte[] body = buffer.getBytes(); | ||
| 257 | - byte[] fullFrame = new byte[mbapHeader.length + body.length]; | ||
| 258 | - System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length); | ||
| 259 | - System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length); | ||
| 260 | - | ||
| 261 | - IotModbusFrame frame = FRAME_CODEC.decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP); | ||
| 262 | - future.complete(frame); | ||
| 263 | - } else { | ||
| 264 | - // Phase 1: 收到 MBAP 头 6 字节 | ||
| 265 | - this.mbapHeader = buffer.getBytes(); | ||
| 266 | - int length = ((mbapHeader[4] & 0xFF) << 8) | (mbapHeader[5] & 0xFF); | ||
| 267 | - this.waitingForBody = true; | ||
| 268 | - parser.fixedSizeMode(length); | ||
| 269 | - } | ||
| 270 | - } catch (Exception e) { | ||
| 271 | - future.completeExceptionally(e); | ||
| 272 | - } | ||
| 273 | - } | ||
| 274 | - } | ||
| 275 | - | ||
| 276 | - /** | ||
| 277 | * 构造认证帧(MODBUS_TCP 格式) | 253 | * 构造认证帧(MODBUS_TCP 格式) |
| 278 | * <p> | 254 | * <p> |
| 279 | * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}} | 255 | * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}} |
| @@ -286,7 +262,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | @@ -286,7 +262,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { | ||
| 286 | JSONObject json = new JSONObject(); | 262 | JSONObject json = new JSONObject(); |
| 287 | json.set("method", "auth"); | 263 | json.set("method", "auth"); |
| 288 | json.set("params", params); | 264 | json.set("params", params); |
| 289 | - return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(), | 265 | + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), |
| 290 | IotModbusFrameFormatEnum.MODBUS_TCP, 1); | 266 | IotModbusFrameFormatEnum.MODBUS_TCP, 1); |
| 291 | } | 267 | } |
| 292 | 268 |