diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java index a80471b..ca317ec 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java @@ -4,7 +4,6 @@ import cn.hutool.core.lang.Assert; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; @@ -12,9 +11,8 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusRecordParserFactory; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream.IotModbusTcpSlaveUpstreamHandler; @@ -23,6 +21,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotM import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; import io.vertx.core.net.NetServer; @@ -33,19 +32,15 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -// TODO @AI:不用主动上报! +// DONE @AI:不用主动上报! /** * IoT 网关 Modbus TCP Slave 协议 *
* 作为 TCP Server 接收设备主动连接:
* 1. 设备通过自定义功能码(FC 65)发送认证请求
- * 2. 认证成功后,根据设备配置的 mode 决定工作模式:
- * - mode=1(云端轮询):网关主动发送 Modbus 读请求,设备响应
- * - mode=2(主动上报):设备主动上报数据,网关透传
+ * 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式)
*
* @author 芋道源码
*/
@@ -85,20 +80,16 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
*/
private Long requestCleanupTimerId;
- /**
- * 未认证连接的帧格式缓存:socket → 检测到的帧格式
- */
- private final Map
- * 纯 Modbus 协议编解码,不处理 TCP 粘包(由 RecordParser 处理)。
- * 支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式,以及自定义功能码扩展。
- *
- * @author 芋道源码
- */
-@Slf4j
-public class IotModbusFrameCodec {
-
- private final int customFunctionCode;
-
- public IotModbusFrameCodec(int customFunctionCode) {
- this.customFunctionCode = customFunctionCode;
- }
-
- // ==================== 解码 ====================
-
- /**
- * 解码响应帧(拆包后的完整帧 byte[])
- *
- * @param data 完整帧字节数组
- * @param format 帧格式
- * @return 解码后的 IotModbusFrame
- */
- public IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) {
- if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
- return decodeTcpResponse(data);
- } else {
- return decodeRtuResponse(data);
- }
- }
-
- /**
- * 解码 MODBUS_TCP 响应
- * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...]
- */
- private IotModbusFrame decodeTcpResponse(byte[] data) {
- if (data.length < 8) {
- log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length);
- return null;
- }
- ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
- int transactionId = buf.getShort() & 0xFFFF;
- buf.getShort(); // protocolId(跳过)// TODO @AI:跳过原因,最好写下;
- buf.getShort(); // length(跳过)// TODO @AI:跳过原因,最好写下;
- int slaveId = buf.get() & 0xFF;
- int functionCode = buf.get() & 0xFF;
- // 提取 PDU 数据(从 functionCode 之后到末尾)
- byte[] pdu = new byte[data.length - 8];
- System.arraycopy(data, 8, pdu, 0, pdu.length);
-
- // 构建 IotModbusFrame
- return buildFrame(slaveId, functionCode, pdu, transactionId);
- }
-
- /**
- * 解码 MODBUS_RTU 响应
- * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)]
- */
- private IotModbusFrame decodeRtuResponse(byte[] data) {
- if (data.length < 4) {
- log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length);
- return null;
- }
- // 校验 CRC
- if (!verifyCrc16(data)) {
- log.warn("[decodeRtuResponse][CRC 校验失败]");
- return null;
- }
- int slaveId = data[0] & 0xFF;
- int functionCode = data[1] & 0xFF;
- // PDU 数据(不含 slaveId、functionCode、CRC)
- byte[] pdu = new byte[data.length - 4];
- System.arraycopy(data, 2, pdu, 0, pdu.length);
-
- // 构建 IotModbusFrame
- return buildFrame(slaveId, functionCode, pdu, null);
- }
-
- /**
- * 构建 IotModbusFrame
- */
- private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) {
- IotModbusFrame frame = new IotModbusFrame()
- .setSlaveId(slaveId)
- .setFunctionCode(functionCode)
- .setPdu(pdu)
- .setTransactionId(transactionId);
-
- // 异常响应
- // TODO @AI:0x80 看看是不是要枚举;
- if ((functionCode & 0x80) != 0) {
- frame.setException(true);
- // TODO @AI:0x7f 看看是不是要枚举;
- frame.setFunctionCode(functionCode & 0x7F);
- if (pdu.length >= 1) {
- frame.setExceptionCode(pdu[0] & 0xFF);
- }
- return frame;
- }
-
- // 自定义功能码
- if (functionCode == customFunctionCode) {
- // data 区格式:[byteCount(1)] [JSON data(N)]
- if (pdu.length >= 1) {
- int byteCount = pdu[0] & 0xFF;
- if (pdu.length >= 1 + byteCount) {
- frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8));
- }
- }
- }
- return frame;
- }
-
- // ==================== 编码 ====================
-
- /**
- * 编码读请求
- *
- * @param slaveId 从站地址
- * @param functionCode 功能码
- * @param startAddress 起始寄存器地址
- * @param quantity 寄存器数量
- * @param format 帧格式
- * @param transactionId 事务 ID(TCP 模式下使用)
- * @return 编码后的字节数组
- */
- public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity,
- IotModbusFrameFormatEnum format, int transactionId) {
- // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)]
- byte[] pdu = new byte[5];
- pdu[0] = (byte) functionCode;
- pdu[1] = (byte) ((startAddress >> 8) & 0xFF);
- pdu[2] = (byte) (startAddress & 0xFF);
- pdu[3] = (byte) ((quantity >> 8) & 0xFF);
- pdu[4] = (byte) (quantity & 0xFF);
- return wrapFrame(slaveId, pdu, format, transactionId);
- }
-
- /**
- * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05)
- *
- * @param slaveId 从站地址
- * @param functionCode 功能码
- * @param address 寄存器地址
- * @param value 值
- * @param format 帧格式
- * @param transactionId 事务 ID
- * @return 编码后的字节数组
- */
- public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value,
- IotModbusFrameFormatEnum format, int transactionId) {
- // PDU: [FC(1)] [Address(2)] [Value(2)]
- byte[] pdu = new byte[5];
- pdu[0] = (byte) functionCode;
- pdu[1] = (byte) ((address >> 8) & 0xFF);
- pdu[2] = (byte) (address & 0xFF);
- pdu[3] = (byte) ((value >> 8) & 0xFF);
- pdu[4] = (byte) (value & 0xFF);
- return wrapFrame(slaveId, pdu, format, transactionId);
- }
-
- /**
- * 编码写多个寄存器请求(FC16)
- *
- * @param slaveId 从站地址
- * @param address 起始地址
- * @param values 值数组
- * @param format 帧格式
- * @param transactionId 事务 ID
- * @return 编码后的字节数组
- */
- public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values,
- IotModbusFrameFormatEnum format, int transactionId) {
- // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)]
- int quantity = values.length;
- int byteCount = quantity * 2;
- byte[] pdu = new byte[6 + byteCount];
- pdu[0] = (byte) 16; // FC16
- pdu[1] = (byte) ((address >> 8) & 0xFF);
- pdu[2] = (byte) (address & 0xFF);
- pdu[3] = (byte) ((quantity >> 8) & 0xFF);
- pdu[4] = (byte) (quantity & 0xFF);
- pdu[5] = (byte) byteCount;
- for (int i = 0; i < quantity; i++) {
- pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF);
- pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF);
- }
- return wrapFrame(slaveId, pdu, format, transactionId);
- }
-
- /**
- * 编码自定义功能码帧(认证响应等)
- *
- * @param slaveId 从站地址
- * @param jsonData JSON 数据
- * @param format 帧格式
- * @param transactionId 事务 ID
- * @return 编码后的字节数组
- */
- public byte[] encodeCustomFrame(int slaveId, String jsonData,
- IotModbusFrameFormatEnum format, int transactionId) {
- byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8);
- // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)]
- byte[] pdu = new byte[2 + jsonBytes.length];
- pdu[0] = (byte) customFunctionCode;
- pdu[1] = (byte) jsonBytes.length;
- System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length);
- return wrapFrame(slaveId, pdu, format, transactionId);
- }
-
- // ==================== 帧封装 ====================
-
- /**
- * 将 PDU 封装为完整帧
- *
- * @param slaveId 从站地址
- * @param pdu PDU 数据(含 functionCode)
- * @param format 帧格式
- * @param transactionId 事务 ID(TCP 模式下使用)
- * @return 完整帧字节数组
- */
- private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, int transactionId) {
- if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
- return wrapTcpFrame(slaveId, pdu, transactionId);
- } else {
- return wrapRtuFrame(slaveId, pdu);
- }
- }
-
- /**
- * 封装 MODBUS_TCP 帧
- * [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...]
- */
- private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) {
- int length = 1 + pdu.length; // UnitId + PDU
- byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU
- // MBAP Header
- frame[0] = (byte) ((transactionId >> 8) & 0xFF);
- frame[1] = (byte) (transactionId & 0xFF);
- frame[2] = 0; // Protocol ID high
- frame[3] = 0; // Protocol ID low
- frame[4] = (byte) ((length >> 8) & 0xFF);
- frame[5] = (byte) (length & 0xFF);
- // Unit ID
- frame[6] = (byte) slaveId;
- // PDU
- System.arraycopy(pdu, 0, frame, 7, pdu.length);
- return frame;
- }
-
- /**
- * 封装 MODBUS_RTU 帧
- * [SlaveId(1)] [PDU...] [CRC(2)]
- */
- private byte[] wrapRtuFrame(int slaveId, byte[] pdu) {
- byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC
- frame[0] = (byte) slaveId;
- System.arraycopy(pdu, 0, frame, 1, pdu.length);
- // 计算并追加 CRC16
- int crc = calculateCrc16(frame, frame.length - 2);
- frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low
- frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
- return frame;
- }
-
- // ==================== CRC16 工具 ====================
-
- // TODO @AI:hutool 等,有没工具类可以用
- /**
- * 计算 CRC-16/MODBUS
- *
- * @param data 数据
- * @param length 计算长度
- * @return CRC16 值
- */
- public static int calculateCrc16(byte[] data, int length) {
- int crc = 0xFFFF;
- for (int i = 0; i < length; i++) {
- crc ^= (data[i] & 0xFF);
- for (int j = 0; j < 8; j++) {
- if ((crc & 0x0001) != 0) {
- crc >>= 1;
- crc ^= 0xA001;
- } else {
- crc >>= 1;
- }
- }
- }
- return crc;
- }
-
- // TODO @AI:hutool 等,有没工具类可以用
- /**
- * 校验 CRC16
- */
- private boolean verifyCrc16(byte[] data) {
- if (data.length < 3) {
- return false;
- }
- int computed = calculateCrc16(data, data.length - 2);
- int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
- return computed == received;
- }
-
-}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java
new file mode 100644
index 0000000..8ea4737
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java
@@ -0,0 +1,451 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
+
+import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
+import io.vertx.core.Handler;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.parsetools.RecordParser;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.function.BiConsumer;
+
+/**
+ * IoT Modbus 帧解码器:集成 TCP 拆包 + 帧格式探测 + 帧解码,一条龙完成从 TCP 字节流到 IotModbusFrame 的转换。
+ *
+ * 流程:
+ * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU
+ * 2. 检测后切换到对应的拆包 Handler,并将首包 6 字节通过 handleFirstBytes() 交给新 Handler 处理
+ * 3. 拆包完成后解码为 IotModbusFrame,通过回调返回
+ * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动)
+ * - MODBUS_RTU:功能码驱动的状态机
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotModbusFrameDecoder {
+
+ private final int customFunctionCode;
+
+ public IotModbusFrameDecoder(int customFunctionCode) {
+ this.customFunctionCode = customFunctionCode;
+ }
+
+ /**
+ * 创建带自动帧格式检测的 RecordParser
+ *
+ * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式)
+ * @return RecordParser 实例
+ */
+ public RecordParser createRecordParser(BiConsumer
+ * Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length
+ * Phase 2: fixedSizeMode(length) → 读 unitId + PDU
+ */
+ @RequiredArgsConstructor
+ private class TcpFrameHandler implements Handler
+ * 状态机流程:
+ * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode
+ * Phase 2: 根据 functionCode 确定剩余长度:
+ * - 异常响应 (FC & 0x80):fixedSizeMode(3) → exceptionCode(1) + CRC(2)
+ * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2)
+ * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2)
+ * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2)
+ */
+ @RequiredArgsConstructor
+ private class RtuFrameHandler implements Handler
+ * 由于 RTU 首帧被探测阶段消费了 6 字节,这里需要从中提取 slaveId + FC 并根据 FC 处理剩余数据
+ *
+ * @param bytes 探测阶段消费的 6 字节:[slaveId][FC][...4 bytes...]
+ */
+ void handleFirstBytes(byte[] bytes) {
+ this.slaveId = bytes[0];
+ this.functionCode = bytes[1];
+ int fc = functionCode & 0xFF;
+ if ((fc & 0x80) != 0) {
+ // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节
+ // 已有 6 字节(多 1 字节),取前 5 字节组装
+ Buffer frame = Buffer.buffer(5);
+ frame.appendByte(slaveId);
+ frame.appendByte(functionCode);
+ frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC
+ emitFrame(frame);
+ resetToHeader();
+ } else if (isReadResponse(fc) || fc == customFunctionCode) {
+ // 读响应或自定义 FC:bytes[2] = byteCount
+ this.byteCount = bytes[2];
+ int bc = byteCount & 0xFF;
+ // 已有数据:bytes[3..5] = 3 字节
+ // 还需:byteCount + CRC(2) - 3 字节已有
+ int remaining = bc + 2 - 3;
+ if (remaining <= 0) {
+ // 数据已足够,组装完整帧
+ int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC
+ Buffer frame = Buffer.buffer(totalLen);
+ frame.appendByte(slaveId);
+ frame.appendByte(functionCode);
+ frame.appendByte(byteCount);
+ frame.appendBytes(bytes, 3, bc + 2); // data + CRC
+ emitFrame(frame);
+ resetToHeader();
+ } else {
+ // 需要继续读
+ state = STATE_READ_DATA;
+ this.pendingData = Buffer.buffer();
+ this.pendingData.appendBytes(bytes, 3, 3); // 暂存已有的 3 字节
+ this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC
+ parser.fixedSizeMode(remaining);
+ }
+ } else if (isWriteResponse(fc)) {
+ // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节
+ // 已有 6 字节,还需 2 字节
+ state = STATE_WRITE_BODY;
+ this.pendingData = Buffer.buffer();
+ this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节
+ parser.fixedSizeMode(2); // 还需 2 字节(CRC)
+ } else {
+ log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
+ resetToHeader();
+ }
+ }
+
+ @Override
+ public void handle(Buffer buffer) {
+ switch (state) {
+ case STATE_HEADER:
+ handleHeader(buffer);
+ break;
+ case STATE_EXCEPTION_BODY:
+ handleExceptionBody(buffer);
+ break;
+ case STATE_READ_BYTE_COUNT:
+ handleReadByteCount(buffer);
+ break;
+ case STATE_READ_DATA:
+ handleReadData(buffer);
+ break;
+ case STATE_WRITE_BODY:
+ handleWriteBody(buffer);
+ break;
+ default:
+ resetToHeader();
+ }
+ }
+
+ private void handleHeader(Buffer buffer) {
+ byte[] header = buffer.getBytes();
+ this.slaveId = header[0];
+ this.functionCode = header[1];
+ int fc = functionCode & 0xFF;
+ if ((fc & 0x80) != 0) {
+ // 异常响应
+ state = STATE_EXCEPTION_BODY;
+ parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2)
+ } else if (isReadResponse(fc) || fc == customFunctionCode) {
+ // 读响应或自定义 FC
+ state = STATE_READ_BYTE_COUNT;
+ parser.fixedSizeMode(1); // byteCount
+ } else if (isWriteResponse(fc)) {
+ // 写响应
+ state = STATE_WRITE_BODY;
+ pendingData = Buffer.buffer();
+ parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2)
+ } else {
+ log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
+ resetToHeader();
+ }
+ }
+
+ private void handleExceptionBody(Buffer buffer) {
+ // buffer = exceptionCode(1) + CRC(2)
+ Buffer frame = Buffer.buffer();
+ frame.appendByte(slaveId);
+ frame.appendByte(functionCode);
+ frame.appendBuffer(buffer);
+ emitFrame(frame);
+ resetToHeader();
+ }
+
+ private void handleReadByteCount(Buffer buffer) {
+ this.byteCount = buffer.getByte(0);
+ int bc = byteCount & 0xFF;
+ state = STATE_READ_DATA;
+ pendingData = Buffer.buffer();
+ expectedDataLen = bc + 2; // data(bc) + CRC(2)
+ parser.fixedSizeMode(expectedDataLen);
+ }
+
+ private void handleReadData(Buffer buffer) {
+ pendingData.appendBuffer(buffer);
+ if (pendingData.length() >= expectedDataLen) {
+ // 组装完整帧
+ Buffer frame = Buffer.buffer();
+ frame.appendByte(slaveId);
+ frame.appendByte(functionCode);
+ frame.appendByte(byteCount);
+ frame.appendBuffer(pendingData);
+ emitFrame(frame);
+ resetToHeader();
+ }
+ // 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode)
+ }
+
+ private void handleWriteBody(Buffer buffer) {
+ pendingData.appendBuffer(buffer);
+ // 完整帧
+ Buffer frame = Buffer.buffer();
+ frame.appendByte(slaveId);
+ frame.appendByte(functionCode);
+ frame.appendBuffer(pendingData);
+ emitFrame(frame);
+ resetToHeader();
+ }
+
+ /**
+ * 发射完整帧:解码并回调
+ */
+ private void emitFrame(Buffer frameBuffer) {
+ IotModbusFrame frame = decodeResponse(frameBuffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU);
+ if (frame != null) {
+ frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_RTU);
+ }
+ }
+
+ private void resetToHeader() {
+ state = STATE_HEADER;
+ pendingData = null;
+ parser.fixedSizeMode(2); // slaveId + FC
+ }
+
+ // TODO @AI:可以抽到 IotModbusUtils 里?
+ private boolean isReadResponse(int fc) {
+ return fc >= 1 && fc <= 4;
+ }
+
+ private boolean isWriteResponse(int fc) {
+ return fc == 5 || fc == 6 || fc == 15 || fc == 16;
+ }
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java
new file mode 100644
index 0000000..0790ab3
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java
@@ -0,0 +1,174 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
+
+import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * IoT Modbus 帧编码器
+ *
+ * 负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class IotModbusFrameEncoder {
+
+ private final int customFunctionCode;
+
+ // ==================== 编码 ====================
+
+ /**
+ * 编码读请求
+ *
+ * @param slaveId 从站地址
+ * @param functionCode 功能码
+ * @param startAddress 起始寄存器地址
+ * @param quantity 寄存器数量
+ * @param format 帧格式
+ * @param transactionId 事务 ID(TCP 模式下使用)
+ * @return 编码后的字节数组
+ */
+ public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity,
+ IotModbusFrameFormatEnum format, int transactionId) {
+ // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)]
+ byte[] pdu = new byte[5];
+ pdu[0] = (byte) functionCode;
+ pdu[1] = (byte) ((startAddress >> 8) & 0xFF);
+ pdu[2] = (byte) (startAddress & 0xFF);
+ pdu[3] = (byte) ((quantity >> 8) & 0xFF);
+ pdu[4] = (byte) (quantity & 0xFF);
+ return wrapFrame(slaveId, pdu, format, transactionId);
+ }
+
+ /**
+ * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05)
+ *
+ * @param slaveId 从站地址
+ * @param functionCode 功能码
+ * @param address 寄存器地址
+ * @param value 值
+ * @param format 帧格式
+ * @param transactionId 事务 ID
+ * @return 编码后的字节数组
+ */
+ public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value,
+ IotModbusFrameFormatEnum format, int transactionId) {
+ // PDU: [FC(1)] [Address(2)] [Value(2)]
+ byte[] pdu = new byte[5];
+ pdu[0] = (byte) functionCode;
+ pdu[1] = (byte) ((address >> 8) & 0xFF);
+ pdu[2] = (byte) (address & 0xFF);
+ pdu[3] = (byte) ((value >> 8) & 0xFF);
+ pdu[4] = (byte) (value & 0xFF);
+ return wrapFrame(slaveId, pdu, format, transactionId);
+ }
+
+ /**
+ * 编码写多个寄存器请求(FC16)
+ *
+ * @param slaveId 从站地址
+ * @param address 起始地址
+ * @param values 值数组
+ * @param format 帧格式
+ * @param transactionId 事务 ID
+ * @return 编码后的字节数组
+ */
+ public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values,
+ IotModbusFrameFormatEnum format, int transactionId) {
+ // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)]
+ int quantity = values.length;
+ int byteCount = quantity * 2;
+ byte[] pdu = new byte[6 + byteCount];
+ pdu[0] = (byte) 16; // FC16
+ pdu[1] = (byte) ((address >> 8) & 0xFF);
+ pdu[2] = (byte) (address & 0xFF);
+ pdu[3] = (byte) ((quantity >> 8) & 0xFF);
+ pdu[4] = (byte) (quantity & 0xFF);
+ pdu[5] = (byte) byteCount;
+ for (int i = 0; i < quantity; i++) {
+ pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF);
+ pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF);
+ }
+ return wrapFrame(slaveId, pdu, format, transactionId);
+ }
+
+ /**
+ * 编码自定义功能码帧(认证响应等)
+ *
+ * @param slaveId 从站地址
+ * @param jsonData JSON 数据
+ * @param format 帧格式
+ * @param transactionId 事务 ID
+ * @return 编码后的字节数组
+ */
+ public byte[] encodeCustomFrame(int slaveId, String jsonData,
+ IotModbusFrameFormatEnum format, int transactionId) {
+ byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8);
+ // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)]
+ byte[] pdu = new byte[2 + jsonBytes.length];
+ pdu[0] = (byte) customFunctionCode;
+ pdu[1] = (byte) jsonBytes.length;
+ System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length);
+ return wrapFrame(slaveId, pdu, format, transactionId);
+ }
+
+ // ==================== 帧封装 ====================
+
+ /**
+ * 将 PDU 封装为完整帧
+ *
+ * @param slaveId 从站地址
+ * @param pdu PDU 数据(含 functionCode)
+ * @param format 帧格式
+ * @param transactionId 事务 ID(TCP 模式下使用)
+ * @return 完整帧字节数组
+ */
+ private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, int transactionId) {
+ if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
+ return wrapTcpFrame(slaveId, pdu, transactionId);
+ } else {
+ return wrapRtuFrame(slaveId, pdu);
+ }
+ }
+
+ /**
+ * 封装 MODBUS_TCP 帧
+ * [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...]
+ */
+ private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) {
+ int length = 1 + pdu.length; // UnitId + PDU
+ byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU
+ // MBAP Header
+ frame[0] = (byte) ((transactionId >> 8) & 0xFF);
+ frame[1] = (byte) (transactionId & 0xFF);
+ frame[2] = 0; // Protocol ID high
+ frame[3] = 0; // Protocol ID low
+ frame[4] = (byte) ((length >> 8) & 0xFF);
+ frame[5] = (byte) (length & 0xFF);
+ // Unit ID
+ frame[6] = (byte) slaveId;
+ // PDU
+ System.arraycopy(pdu, 0, frame, 7, pdu.length);
+ return frame;
+ }
+
+ /**
+ * 封装 MODBUS_RTU 帧
+ * [SlaveId(1)] [PDU...] [CRC(2)]
+ */
+ private byte[] wrapRtuFrame(int slaveId, byte[] pdu) {
+ byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC
+ frame[0] = (byte) slaveId;
+ System.arraycopy(pdu, 0, frame, 1, pdu.length);
+ // 计算并追加 CRC16
+ int crc = IotModbusUtils.calculateCrc16(frame, frame.length - 2);
+ frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low
+ frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
+ return frame;
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java
deleted file mode 100644
index 07d98d3..0000000
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusRecordParserFactory.java
+++ /dev/null
@@ -1,362 +0,0 @@
-package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
-
-import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
-import io.vertx.core.Handler;
-import io.vertx.core.buffer.Buffer;
-import io.vertx.core.parsetools.RecordParser;
-import lombok.extern.slf4j.Slf4j;
-
-import java.util.function.Consumer;
-
-// TODO @AI:看看是不是不要搞成 factory,而是直接 new;(可以一起讨论下)
-/**
- * IoT Modbus RecordParser 工厂
- *
- * 创建带自动帧格式检测的 RecordParser:
- * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU
- * 2. 检测后自动切换到对应的拆包模式
- * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动)
- * - MODBUS_RTU:功能码驱动的状态机
- *
- * @author 芋道源码
- */
-@Slf4j
-public class IotModbusRecordParserFactory {
-
- /**
- * 创建带自动帧格式检测的 RecordParser
- *
- * @param customFunctionCode 自定义功能码
- * @param frameHandler 完整帧回调
- * @param onFormatDetected 帧格式检测回调
- * @return RecordParser 实例
- */
- public static RecordParser create(int customFunctionCode,
- Handler
- * 状态机流程:
- * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode
- * Phase 2: 根据 functionCode 确定剩余长度:
- * - 异常响应 (FC & 0x80):fixedSizeMode(3) → exceptionCode(1) + CRC(2)
- * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2)
- * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2)
- * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2)
- */
- private static class RtuFrameHandler implements Handler
- * 从解码后的 IotModbusFrame 中提取寄存器值,用于后续的点位翻译。
+ * 提供:
+ * 1. CRC-16/MODBUS 计算和校验
+ * 2. 从解码后的 IotModbusFrame 中提取寄存器值(用于后续的点位翻译)
*
* @author 芋道源码
*/
@Slf4j
-public class IotModbusResponseParser {
+public class IotModbusUtils {
+
+ // TODO @AI:可以把 1、2、3、4、5 这些 fucntion code 在这里枚举下。
+ // TODO @AI:一些枚举 0x80 这些可以这里枚举;
+
+ // ==================== CRC16 工具 ====================
+
+ /**
+ * 计算 CRC-16/MODBUS
+ *
+ * @param data 数据
+ * @param length 计算长度
+ * @return CRC16 值
+ */
+ public static int calculateCrc16(byte[] data, int length) {
+ int crc = 0xFFFF;
+ for (int i = 0; i < length; i++) {
+ crc ^= (data[i] & 0xFF);
+ for (int j = 0; j < 8; j++) {
+ if ((crc & 0x0001) != 0) {
+ crc >>= 1;
+ crc ^= 0xA001;
+ } else {
+ crc >>= 1;
+ }
+ }
+ }
+ return crc;
+ }
+
+ /**
+ * 校验 CRC16
+ *
+ * @param data 包含 CRC 的完整数据
+ * @return 校验是否通过
+ */
+ public static boolean verifyCrc16(byte[] data) {
+ if (data.length < 3) {
+ return false;
+ }
+ int computed = calculateCrc16(data, data.length - 2);
+ int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
+ return computed == received;
+ }
+
+ // ==================== 响应值提取 ====================
/**
* 从帧中提取寄存器值(FC01-04 读响应)
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
index 76e904e..5a47a09 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
@@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
-import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
+import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
@@ -35,7 +35,7 @@ public class IotModbusTcpSlaveDownstreamHandler {
private final IotModbusTcpSlaveConnectionManager connectionManager;
private final IotModbusTcpSlaveConfigCacheService configCacheService;
private final IotModbusDataConverter dataConverter;
- private final IotModbusFrameCodec frameCodec;
+ private final IotModbusFrameEncoder frameEncoder;
/**
* TCP 事务 ID 自增器
@@ -117,11 +117,11 @@ public class IotModbusTcpSlaveDownstreamHandler {
}
if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) {
// 单个值:使用单写功能码(FC05/FC06)
- data = frameCodec.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(),
+ data = frameEncoder.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(),
point.getRegisterAddress(), rawValues[0], frameFormat, transactionId);
} else if (fcEnum.getWriteMultipleCode() != null) {
// 多个值:使用多写功能码(FC15/FC16)
- data = frameCodec.encodeWriteMultipleRegistersRequest(slaveId,
+ data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId,
point.getRegisterAddress(), rawValues, frameFormat, transactionId);
} else {
log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier());
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java
index 4d91b63..15ae18f 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java
@@ -1,11 +1,13 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.upstream;
+import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
-import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
@@ -19,69 +21,76 @@ import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
-import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameCodec;
-import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusResponseParser;
+import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
+import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest;
+import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePollScheduler;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.net.NetSocket;
-import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
-import java.util.function.BiConsumer;
-// TODO @AI:逻辑有点多,看看是不是分区域!
+import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
+import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
+
+// DONE @AI:逻辑有点多,看看是不是分区域! => 已按区域划分:认证 / 轮询响应
/**
* IoT Modbus TCP Slave 上行数据处理器
*
* 处理:
* 1. 自定义 FC 认证
* 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post
- * 3. 主动上报(mode=2)→ 透传 property.report TODO @AI:这种模式,应该不用支持;因为主动上报,都走标准的 tcp 即可;
+ * // DONE @AI:不用主动上报;主动上报走标准 tcp 即可
*
* @author 芋道源码
*/
@Slf4j
public class IotModbusTcpSlaveUpstreamHandler {
+ private static final String METHOD_AUTH = "auth";
+
private final IotDeviceCommonApi deviceApi;
private final IotDeviceMessageService messageService;
private final IotModbusDataConverter dataConverter;
- private final IotModbusFrameCodec frameCodec;
+ private final IotModbusFrameEncoder frameEncoder;
private final IotModbusTcpSlaveConnectionManager connectionManager;
private final IotModbusTcpSlaveConfigCacheService configCacheService;
private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
+ private final IotModbusTcpSlavePollScheduler pollScheduler;
+ private final IotDeviceService deviceService;
private final String serverId;
- /**
- * 认证成功回调:(deviceId, config) → 启动轮询等
- */
- @Setter
- private BiConsumer
+ * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler}
*/
private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
try {
- // TODO @AI:直接使用 JsonUtils 去解析出 IotDeviceMessage
JSONObject json = JSONUtil.parseObj(frame.getCustomData());
String method = json.getStr("method");
- // TODO @AI: method 枚举下;
- if ("auth".equals(method)) {
+ if (METHOD_AUTH.equals(method)) {
handleAuth(socket, frame, json, frameFormat);
return;
}
- // TODO @AI:把 frame 都打印下;
- log.warn("[handleCustomFrame][未知 method: {}]", method);
+ log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]",
+ method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData());
+ } catch (ServiceException e) {
+ // 已知业务异常,返回对应的错误码和错误信息
+ sendCustomResponse(socket, frame, frameFormat, e.getCode(), e.getMessage());
+ } catch (IllegalArgumentException e) {
+ // 参数校验异常,返回 400 错误
+ sendCustomResponse(socket, frame, frameFormat, BAD_REQUEST.getCode(), e.getMessage());
} catch (Exception e) {
- // TODO @AI:各种情况的翻译;看看怎么弄比较合适;是不是要用 fc 自定义的 callback 下?
- log.error("[handleCustomFrame][解析自定义 FC 数据失败]", e);
+ // 其他未知异常,返回 500 错误
+ log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]",
+ frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e);
+ sendCustomResponse(socket, frame, frameFormat,
+ INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
}
}
+ // TODO @芋艿:在 review 下这个类;
+ // TODO @AI:不传递 json,直接在 frame
/**
* 处理认证请求
*/
private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json,
IotModbusFrameFormatEnum frameFormat) {
- // TODO @AI:参数为空的校验;
+ // TODO @AI:是不是可以 JsonUtils.convert(json, IotDeviceAuthReqDTO.class);
JSONObject params = json.getJSONObject("params");
if (params == null) {
- sendAuthResponse(socket, frame, frameFormat, 1, "params 为空");
- return;
+ throw invalidParamException("params 不能为空");
}
- // TODO @AI:参数判空;
+ // DONE @AI:参数判空
String clientId = params.getStr("clientId");
String username = params.getStr("username");
String password = params.getStr("password");
+ // TODO @AI:逐个判空;
+ if (StrUtil.hasBlank(clientId, username, password)) {
+ throw invalidParamException("clientId、username、password 不能为空");
+ }
- try {
- // 1. 调用认证 API
- IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO()
- .setClientId(clientId).setUsername(username).setPassword(password);
- CommonResult
- * 使用两阶段 RecordParser 拆包:fixedSizeMode(6) 读 MBAP 头 → fixedSizeMode(length) 读 body
+ * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码)
*/
private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
CompletableFuture
* JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}}
@@ -286,7 +262,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest {
JSONObject json = new JSONObject();
json.set("method", "auth");
json.set("params", params);
- return FRAME_CODEC.encodeCustomFrame(SLAVE_ID, json.toString(),
+ return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(),
IotModbusFrameFormatEnum.MODBUS_TCP, 1);
}