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 | 4 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 7 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | |
| 8 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; |
| 9 | 8 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 10 | 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 | 11 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; |
| 13 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 14 | 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 | 16 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; |
| 19 | 17 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber; |
| 20 | 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 | 21 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| 24 | 22 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; |
| 25 | 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 | 25 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 27 | 26 | import io.vertx.core.Vertx; |
| 28 | 27 | import io.vertx.core.net.NetServer; |
| ... | ... | @@ -33,19 +32,15 @@ import lombok.Getter; |
| 33 | 32 | import lombok.extern.slf4j.Slf4j; |
| 34 | 33 | |
| 35 | 34 | import java.util.List; |
| 36 | -import java.util.Map; | |
| 37 | -import java.util.concurrent.ConcurrentHashMap; | |
| 38 | 35 | import java.util.concurrent.TimeUnit; |
| 39 | 36 | |
| 40 | -// TODO @AI:不用主动上报! | |
| 37 | +// DONE @AI:不用主动上报! | |
| 41 | 38 | /** |
| 42 | 39 | * IoT 网关 Modbus TCP Slave 协议 |
| 43 | 40 | * <p> |
| 44 | 41 | * 作为 TCP Server 接收设备主动连接: |
| 45 | 42 | * 1. 设备通过自定义功能码(FC 65)发送认证请求 |
| 46 | - * 2. 认证成功后,根据设备配置的 mode 决定工作模式: | |
| 47 | - * - mode=1(云端轮询):网关主动发送 Modbus 读请求,设备响应 | |
| 48 | - * - mode=2(主动上报):设备主动上报数据,网关透传 | |
| 43 | + * 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式) | |
| 49 | 44 | * |
| 50 | 45 | * @author 芋道源码 |
| 51 | 46 | */ |
| ... | ... | @@ -85,20 +80,16 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { |
| 85 | 80 | */ |
| 86 | 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 | 86 | private final IotModbusTcpSlaveConfig slaveConfig; |
| 96 | - private final IotModbusFrameCodec frameCodec; | |
| 87 | + private final IotModbusFrameDecoder frameDecoder; | |
| 88 | + private final IotModbusFrameEncoder frameEncoder; | |
| 97 | 89 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 98 | 90 | private final IotModbusTcpSlaveConfigCacheService configCacheService; |
| 99 | 91 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; |
| 100 | 92 | private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; |
| 101 | - private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; | |
| 102 | 93 | private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; |
| 103 | 94 | private final IotModbusTcpSlavePollScheduler pollScheduler; |
| 104 | 95 | |
| ... | ... | @@ -118,33 +109,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { |
| 118 | 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 | 116 | this.pollScheduler = new IotModbusTcpSlavePollScheduler( |
| 134 | - vertx, connectionManager, frameCodec, pendingRequestManager, | |
| 117 | + vertx, connectionManager, frameEncoder, pendingRequestManager, | |
| 135 | 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 | 130 | IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); |
| 131 | + IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( | |
| 132 | + connectionManager, configCacheService, dataConverter, frameEncoder); | |
| 148 | 133 | this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( |
| 149 | 134 | this, downstreamHandler, messageBus); |
| 150 | 135 | } |
| ... | ... | @@ -168,6 +153,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { |
| 168 | 153 | |
| 169 | 154 | try { |
| 170 | 155 | // 1.1 首次加载配置 |
| 156 | + // TODO @AI:可能首次不用加载;你在想想; | |
| 171 | 157 | refreshConfig(); |
| 172 | 158 | // 1.2 启动配置刷新定时器 |
| 173 | 159 | int refreshInterval = slaveConfig.getConfigRefreshInterval(); |
| ... | ... | @@ -190,10 +176,21 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { |
| 190 | 176 | downstreamSubscriber.start(); |
| 191 | 177 | } catch (Exception e) { |
| 192 | 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 | 191 | if (vertx != null) { |
| 194 | 192 | vertx.close(); |
| 195 | 193 | } |
| 196 | - // TODO @AI:其它相关的 close; | |
| 197 | 194 | throw e; |
| 198 | 195 | } |
| 199 | 196 | } |
| ... | ... | @@ -252,20 +249,19 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { |
| 252 | 249 | * 启动 TCP Server |
| 253 | 250 | */ |
| 254 | 251 | private void startTcpServer() { |
| 255 | - // TODO @AI:host 一定要设置么? | |
| 256 | 252 | // 1. 创建 TCP Server |
| 257 | 253 | NetServerOptions options = new NetServerOptions() |
| 258 | - .setPort(properties.getPort()) | |
| 259 | - .setHost("0.0.0.0"); | |
| 254 | + .setPort(properties.getPort()); | |
| 260 | 255 | netServer = vertx.createNetServer(options); |
| 261 | 256 | |
| 262 | 257 | // 2. 设置连接处理器 |
| 263 | 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 | 270 | private void handleConnection(NetSocket socket) { |
| 275 | 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 | 280 | // 2.1 连接关闭处理 |
| 317 | 281 | socket.closeHandler(v -> { |
| 318 | - pendingFrameFormats.remove(socket); | |
| 319 | 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 | 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 | 292 | // 2.2 异常处理 |
| 331 | 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 | 3 | import lombok.extern.slf4j.Slf4j; |
| 4 | 4 | |
| 5 | 5 | /** |
| 6 | - * IoT Modbus 响应值提取器 | |
| 6 | + * IoT Modbus 工具类 | |
| 7 | 7 | * <p> |
| 8 | - * 从解码后的 IotModbusFrame 中提取寄存器值,用于后续的点位翻译。 | |
| 8 | + * 提供: | |
| 9 | + * 1. CRC-16/MODBUS 计算和校验 | |
| 10 | + * 2. 从解码后的 IotModbusFrame 中提取寄存器值(用于后续的点位翻译) | |
| 9 | 11 | * |
| 10 | 12 | * @author 芋道源码 |
| 11 | 13 | */ |
| 12 | 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 | 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 | 8 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 10 | 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 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; |
| 13 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; |
| 14 | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| ... | ... | @@ -35,7 +35,7 @@ public class IotModbusTcpSlaveDownstreamHandler { |
| 35 | 35 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 36 | 36 | private final IotModbusTcpSlaveConfigCacheService configCacheService; |
| 37 | 37 | private final IotModbusDataConverter dataConverter; |
| 38 | - private final IotModbusFrameCodec frameCodec; | |
| 38 | + private final IotModbusFrameEncoder frameEncoder; | |
| 39 | 39 | |
| 40 | 40 | /** |
| 41 | 41 | * TCP 事务 ID 自增器 |
| ... | ... | @@ -117,11 +117,11 @@ public class IotModbusTcpSlaveDownstreamHandler { |
| 117 | 117 | } |
| 118 | 118 | if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) { |
| 119 | 119 | // 单个值:使用单写功能码(FC05/FC06) |
| 120 | - data = frameCodec.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), | |
| 120 | + data = frameEncoder.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), | |
| 121 | 121 | point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); |
| 122 | 122 | } else if (fcEnum.getWriteMultipleCode() != null) { |
| 123 | 123 | // 多个值:使用多写功能码(FC15/FC16) |
| 124 | - data = frameCodec.encodeWriteMultipleRegistersRequest(slaveId, | |
| 124 | + data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, | |
| 125 | 125 | point.getRegisterAddress(), rawValues, frameFormat, transactionId); |
| 126 | 126 | } else { |
| 127 | 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 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 3 | 4 | import cn.hutool.core.lang.Assert; |
| 4 | 5 | import cn.hutool.core.map.MapUtil; |
| 6 | +import cn.hutool.core.util.BooleanUtil; | |
| 5 | 7 | import cn.hutool.core.util.StrUtil; |
| 6 | -import cn.hutool.extra.spring.SpringUtil; | |
| 7 | 8 | import cn.hutool.json.JSONObject; |
| 8 | 9 | import cn.hutool.json.JSONUtil; |
| 10 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 9 | 11 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 10 | 12 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 11 | 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 | 21 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 20 | 22 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; |
| 21 | 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 | 26 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; |
| 25 | 27 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; |
| 26 | 28 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| 27 | 29 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; |
| 28 | 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 | 32 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 30 | 33 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 31 | 34 | import io.vertx.core.net.NetSocket; |
| 32 | -import lombok.Setter; | |
| 33 | 35 | import lombok.extern.slf4j.Slf4j; |
| 34 | 36 | |
| 35 | 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 | 45 | * IoT Modbus TCP Slave 上行数据处理器 |
| 41 | 46 | * <p> |
| 42 | 47 | * 处理: |
| 43 | 48 | * 1. 自定义 FC 认证 |
| 44 | 49 | * 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post |
| 45 | - * 3. 主动上报(mode=2)→ 透传 property.report TODO @AI:这种模式,应该不用支持;因为主动上报,都走标准的 tcp 即可; | |
| 50 | + * // DONE @AI:不用主动上报;主动上报走标准 tcp 即可 | |
| 46 | 51 | * |
| 47 | 52 | * @author 芋道源码 |
| 48 | 53 | */ |
| 49 | 54 | @Slf4j |
| 50 | 55 | public class IotModbusTcpSlaveUpstreamHandler { |
| 51 | 56 | |
| 57 | + private static final String METHOD_AUTH = "auth"; | |
| 58 | + | |
| 52 | 59 | private final IotDeviceCommonApi deviceApi; |
| 53 | 60 | private final IotDeviceMessageService messageService; |
| 54 | 61 | private final IotModbusDataConverter dataConverter; |
| 55 | - private final IotModbusFrameCodec frameCodec; | |
| 62 | + private final IotModbusFrameEncoder frameEncoder; | |
| 56 | 63 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 57 | 64 | private final IotModbusTcpSlaveConfigCacheService configCacheService; |
| 58 | 65 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; |
| 66 | + private final IotModbusTcpSlavePollScheduler pollScheduler; | |
| 67 | + private final IotDeviceService deviceService; | |
| 59 | 68 | private final String serverId; |
| 60 | 69 | |
| 61 | - /** | |
| 62 | - * 认证成功回调:(deviceId, config) → 启动轮询等 | |
| 63 | - */ | |
| 64 | - @Setter | |
| 65 | - private BiConsumer<Long, IotModbusDeviceConfigRespDTO> onAuthSuccess; | |
| 66 | - | |
| 67 | 70 | public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, |
| 68 | 71 | IotDeviceMessageService messageService, |
| 69 | 72 | IotModbusDataConverter dataConverter, |
| 70 | - IotModbusFrameCodec frameCodec, | |
| 73 | + IotModbusFrameEncoder frameEncoder, | |
| 71 | 74 | IotModbusTcpSlaveConnectionManager connectionManager, |
| 72 | 75 | IotModbusTcpSlaveConfigCacheService configCacheService, |
| 73 | 76 | IotModbusTcpSlavePendingRequestManager pendingRequestManager, |
| 77 | + IotModbusTcpSlavePollScheduler pollScheduler, | |
| 78 | + IotDeviceService deviceService, | |
| 74 | 79 | String serverId) { |
| 75 | 80 | this.deviceApi = deviceApi; |
| 76 | 81 | this.messageService = messageService; |
| 77 | 82 | this.dataConverter = dataConverter; |
| 78 | - this.frameCodec = frameCodec; | |
| 83 | + this.frameEncoder = frameEncoder; | |
| 79 | 84 | this.connectionManager = connectionManager; |
| 80 | 85 | this.configCacheService = configCacheService; |
| 81 | 86 | this.pendingRequestManager = pendingRequestManager; |
| 87 | + this.pollScheduler = pollScheduler; | |
| 88 | + this.deviceService = deviceService; | |
| 82 | 89 | this.serverId = serverId; |
| 83 | 90 | } |
| 84 | 91 | |
| 92 | + // ========== 帧处理入口 ========== | |
| 93 | + | |
| 85 | 94 | /** |
| 86 | 95 | * 处理帧 |
| 87 | 96 | */ |
| ... | ... | @@ -89,159 +98,158 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 89 | 98 | if (frame == null) { |
| 90 | 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 | 102 | if (frame.isException()) { |
| 99 | - // TODO @AI:这种需要返回一个结果给 modbus client? | |
| 100 | 103 | log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]", |
| 101 | 104 | frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode()); |
| 102 | 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 | 116 | if (!connectionManager.isAuthenticated(socket)) { |
| 106 | - // TODO @AI:这种需要返回一个结果给 modbus client? | |
| 107 | 117 | log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); |
| 108 | 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 | 125 | ConnectionInfo info = connectionManager.getConnectionInfo(socket); |
| 114 | 126 | if (info == null) { |
| 127 | + log.warn("[handleFrame][已认证但连接信息为空, remoteAddress={}]", socket.remoteAddress()); | |
| 115 | 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 | 140 | private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { |
| 131 | 141 | try { |
| 132 | - // TODO @AI:直接使用 JsonUtils 去解析出 IotDeviceMessage | |
| 133 | 142 | JSONObject json = JSONUtil.parseObj(frame.getCustomData()); |
| 134 | 143 | String method = json.getStr("method"); |
| 135 | - // TODO @AI: method 枚举下; | |
| 136 | - if ("auth".equals(method)) { | |
| 144 | + if (METHOD_AUTH.equals(method)) { | |
| 137 | 145 | handleAuth(socket, frame, json, frameFormat); |
| 138 | 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 | 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 | 170 | private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json, |
| 152 | 171 | IotModbusFrameFormatEnum frameFormat) { |
| 153 | - // TODO @AI:参数为空的校验; | |
| 172 | + // TODO @AI:是不是可以 JsonUtils.convert(json, IotDeviceAuthReqDTO.class); | |
| 154 | 173 | JSONObject params = json.getJSONObject("params"); |
| 155 | 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 | 178 | String clientId = params.getStr("clientId"); |
| 161 | 179 | String username = params.getStr("username"); |
| 162 | 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 | 240 | JSONObject resp = new JSONObject(); |
| 235 | - resp.set("method", "auth"); | |
| 241 | + resp.set("method", METHOD_AUTH); | |
| 236 | 242 | resp.set("code", code); |
| 237 | 243 | resp.set("message", message); |
| 238 | - byte[] data = frameCodec.encodeCustomFrame(frame.getSlaveId(), resp.toString(), | |
| 244 | + byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), resp.toString(), | |
| 239 | 245 | frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); |
| 240 | 246 | connectionManager.sendToSocket(socket, data); |
| 241 | 247 | } |
| 242 | 248 | |
| 249 | + // ========== 轮询响应处理 ========== | |
| 250 | + | |
| 243 | 251 | /** |
| 244 | - * 处理轮询响应(mode=1) | |
| 252 | + * 处理轮询响应(云端轮询模式) | |
| 245 | 253 | */ |
| 246 | 254 | private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame, |
| 247 | 255 | IotModbusFrameFormatEnum frameFormat) { |
| ... | ... | @@ -254,7 +262,7 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 254 | 262 | return; |
| 255 | 263 | } |
| 256 | 264 | // 1.2 提取寄存器值 |
| 257 | - int[] rawValues = IotModbusResponseParser.extractValues(frame); | |
| 265 | + int[] rawValues = IotModbusUtils.extractValues(frame); | |
| 258 | 266 | if (rawValues == null) { |
| 259 | 267 | log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", |
| 260 | 268 | info.getDeviceId(), request.getIdentifier()); |
| ... | ... | @@ -262,20 +270,17 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 262 | 270 | } |
| 263 | 271 | // 1.3 查找点位配置 |
| 264 | 272 | IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); |
| 265 | - if (config == null || config.getPoints() == null) { | |
| 273 | + if (config == null || CollUtil.isEmpty(config.getPoints())) { | |
| 266 | 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 | 277 | if (point == null) { |
| 273 | 278 | return; |
| 274 | 279 | } |
| 275 | 280 | |
| 276 | - // TODO @AI:拆成 2.1、2.2 | |
| 277 | - // 4. 点位翻译 → 上报 | |
| 281 | + // 2.1 点位翻译 | |
| 278 | 282 | Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); |
| 283 | + // 2.2 上报属性 | |
| 279 | 284 | Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue); |
| 280 | 285 | IotDeviceMessage message = IotDeviceMessage.requestOf( |
| 281 | 286 | IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); |
| ... | ... | @@ -284,29 +289,4 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 284 | 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 | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 6 | 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 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest; |
| 10 | 10 | import io.vertx.core.Vertx; |
| ... | ... | @@ -37,7 +37,7 @@ public class IotModbusTcpSlavePollScheduler { |
| 37 | 37 | |
| 38 | 38 | private final Vertx vertx; |
| 39 | 39 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 40 | - private final IotModbusFrameCodec frameCodec; | |
| 40 | + private final IotModbusFrameEncoder frameEncoder; | |
| 41 | 41 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; |
| 42 | 42 | private final int requestTimeout; |
| 43 | 43 | |
| ... | ... | @@ -163,7 +163,7 @@ public class IotModbusTcpSlavePollScheduler { |
| 163 | 163 | int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; |
| 164 | 164 | int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; |
| 165 | 165 | // 2.2 编码读请求 |
| 166 | - byte[] data = frameCodec.encodeReadRequest(slaveId, point.getFunctionCode(), | |
| 166 | + byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), | |
| 167 | 167 | point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); |
| 168 | 168 | // 2.3 注册 PendingRequest |
| 169 | 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 | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 8 | 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 | 12 | import io.vertx.core.Vertx; |
| 12 | 13 | import io.vertx.core.buffer.Buffer; |
| 13 | 14 | import io.vertx.core.net.NetClient; |
| ... | ... | @@ -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 | 66 | // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== |
| 65 | 67 | |
| ... | ... | @@ -111,7 +113,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { |
| 111 | 113 | // ===================== 轮询响应测试 ===================== |
| 112 | 114 | |
| 113 | 115 | /** |
| 114 | - * 轮询响应测试:认证后等待网关下发 FC03 读请求(RTU 格式),构造读响应帧发回 | |
| 116 | + * 轮询响应测试:认证后持续监听网关下发的读请求(RTU 格式),每次收到都自动构造读响应帧发回 | |
| 115 | 117 | */ |
| 116 | 118 | @Test |
| 117 | 119 | public void testPollingResponse() throws Exception { |
| ... | ... | @@ -121,30 +123,32 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { |
| 121 | 123 | IotModbusFrame authResponse = authenticate(socket); |
| 122 | 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 | 152 | } finally { |
| 149 | 153 | socket.close(); |
| 150 | 154 | } |
| ... | ... | @@ -199,23 +203,20 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { |
| 199 | 203 | } |
| 200 | 204 | |
| 201 | 205 | /** |
| 202 | - * 发送帧并等待响应(使用 IotModbusRecordParserFactory 自动检测帧格式) | |
| 206 | + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码) | |
| 203 | 207 | */ |
| 204 | 208 | private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { |
| 205 | 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 | 213 | try { |
| 210 | - // 检测到的帧格式应该是 RTU,使用 RTU 格式解码 | |
| 211 | - IotModbusFrame frame = FRAME_CODEC.decodeResponse( | |
| 212 | - buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); | |
| 214 | + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat); | |
| 213 | 215 | responseFuture.complete(frame); |
| 214 | 216 | } catch (Exception e) { |
| 215 | 217 | responseFuture.completeExceptionally(e); |
| 216 | 218 | } |
| 217 | - }, | |
| 218 | - format -> log.info("[sendAndReceive][检测到帧格式: {}]", format)); | |
| 219 | + }); | |
| 219 | 220 | socket.handler(parser); |
| 220 | 221 | |
| 221 | 222 | // 发送请求 |
| ... | ... | @@ -231,18 +232,16 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { |
| 231 | 232 | */ |
| 232 | 233 | private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { |
| 233 | 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 | 238 | try { |
| 238 | - IotModbusFrame frame = FRAME_CODEC.decodeResponse( | |
| 239 | - buffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU); | |
| 239 | + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat); | |
| 240 | 240 | requestFuture.complete(frame); |
| 241 | 241 | } catch (Exception e) { |
| 242 | 242 | requestFuture.completeExceptionally(e); |
| 243 | 243 | } |
| 244 | - }, | |
| 245 | - format -> log.info("[waitForRequest][检测到帧格式: {}]", format)); | |
| 244 | + }); | |
| 246 | 245 | socket.handler(parser); |
| 247 | 246 | |
| 248 | 247 | // 等待(超时 30 秒,因为轮询间隔可能比较长) |
| ... | ... | @@ -264,7 +263,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { |
| 264 | 263 | JSONObject json = new JSONObject(); |
| 265 | 264 | json.set("method", "auth"); |
| 266 | 265 | json.set("params", params); |
| 267 | - return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(), | |
| 266 | + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), | |
| 268 | 267 | IotModbusFrameFormatEnum.MODBUS_RTU, 0); |
| 269 | 268 | } |
| 270 | 269 | |
| ... | ... | @@ -286,7 +285,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest { |
| 286 | 285 | frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF); |
| 287 | 286 | } |
| 288 | 287 | // 计算 CRC16 |
| 289 | - int crc = IotModbusFrameCodec.calculateCrc16(frame, totalLength - 2); | |
| 288 | + int crc = IotModbusUtils.calculateCrc16(frame, totalLength - 2); | |
| 290 | 289 | frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low |
| 291 | 290 | frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High |
| 292 | 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 | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 8 | 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 | 11 | import io.vertx.core.Vertx; |
| 12 | 12 | import io.vertx.core.buffer.Buffer; |
| 13 | 13 | import io.vertx.core.net.NetClient; |
| ... | ... | @@ -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 | 67 | // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) ===================== |
| 67 | 68 | |
| ... | ... | @@ -113,7 +114,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { |
| 113 | 114 | // ===================== 轮询响应测试 ===================== |
| 114 | 115 | |
| 115 | 116 | /** |
| 116 | - * 轮询响应测试:认证后等待网关下发 FC03 读请求,构造读响应帧发回 | |
| 117 | + * 轮询响应测试:认证后持续监听网关下发的读请求,每次收到都自动构造读响应帧发回 | |
| 117 | 118 | */ |
| 118 | 119 | @Test |
| 119 | 120 | public void testPollingResponse() throws Exception { |
| ... | ... | @@ -123,29 +124,31 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { |
| 123 | 124 | IotModbusFrame authResponse = authenticate(socket); |
| 124 | 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 | 152 | Thread.sleep(200000); |
| 150 | 153 | } finally { |
| 151 | 154 | socket.close(); |
| ... | ... | @@ -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 | 209 | private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception { |
| 209 | 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 | 221 | socket.handler(parser); |
| 214 | 222 | |
| 215 | 223 | // 发送请求 |
| ... | ... | @@ -225,8 +233,16 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { |
| 225 | 233 | */ |
| 226 | 234 | private IotModbusFrame waitForRequest(NetSocket socket) throws Exception { |
| 227 | 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 | 246 | socket.handler(parser); |
| 231 | 247 | |
| 232 | 248 | // 等待(超时 30 秒,因为轮询间隔可能比较长) |
| ... | ... | @@ -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 | 253 | * 构造认证帧(MODBUS_TCP 格式) |
| 278 | 254 | * <p> |
| 279 | 255 | * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}} |
| ... | ... | @@ -286,7 +262,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { |
| 286 | 262 | JSONObject json = new JSONObject(); |
| 287 | 263 | json.set("method", "auth"); |
| 288 | 264 | json.set("params", params); |
| 289 | - return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(), | |
| 265 | + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(), | |
| 290 | 266 | IotModbusFrameFormatEnum.MODBUS_TCP, 1); |
| 291 | 267 | } |
| 292 | 268 | ... | ... |