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