Commit 346ae3ff48d3ac31b25cad6dbbe03457f90cbfa8

Authored by YunaiV
1 parent a0db8684

feat(iot):modbus-tcp-slave 优化它的 code、decode 逻辑;

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