Commit 3ab33527e3d3ea0c7a11789d9585e3079e9ee46d
1 parent
346ae3ff
feat(iot):modbus-tcp-slave 整体代码进一步优化
Showing
17 changed files
with
427 additions
and
479 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java
| @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; | ||
| 3 | import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; | 3 | import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; |
| 4 | import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; | 4 | import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; |
| 5 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; |
| 6 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; | ||
| 7 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; |
| 8 | import com.baomidou.mybatisplus.annotation.KeySequence; | 7 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 9 | import com.baomidou.mybatisplus.annotation.TableId; | 8 | import com.baomidou.mybatisplus.annotation.TableId; |
| @@ -62,7 +61,7 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { | @@ -62,7 +61,7 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { | ||
| 62 | /** | 61 | /** |
| 63 | * Modbus 功能码 | 62 | * Modbus 功能码 |
| 64 | * | 63 | * |
| 65 | - * 枚举 {@link IotModbusFunctionCodeEnum} | 64 | + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器) |
| 66 | */ | 65 | */ |
| 67 | private Integer functionCode; | 66 | private Integer functionCode; |
| 68 | /** | 67 | /** |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java
| 1 | package cn.iocoder.yudao.module.iot.core.biz.dto; | 1 | package cn.iocoder.yudao.module.iot.core.biz.dto; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; |
| 4 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; | 4 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; |
| 6 | import lombok.Data; | 5 | import lombok.Data; |
| 7 | 6 | ||
| @@ -33,7 +32,7 @@ public class IotModbusPointRespDTO { | @@ -33,7 +32,7 @@ public class IotModbusPointRespDTO { | ||
| 33 | /** | 32 | /** |
| 34 | * Modbus 功能码 | 33 | * Modbus 功能码 |
| 35 | * | 34 | * |
| 36 | - * 枚举 {@link IotModbusFunctionCodeEnum} | 35 | + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器) |
| 37 | */ | 36 | */ |
| 38 | private Integer functionCode; | 37 | private Integer functionCode; |
| 39 | /** | 38 | /** |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFunctionCodeEnum.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.core.enums; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.common.core.ArrayValuable; | ||
| 4 | -import lombok.Getter; | ||
| 5 | -import lombok.RequiredArgsConstructor; | ||
| 6 | - | ||
| 7 | -import java.util.Arrays; | ||
| 8 | - | ||
| 9 | -/** | ||
| 10 | - * IoT Modbus 功能码枚举 | ||
| 11 | - * | ||
| 12 | - * @author 芋道源码 | ||
| 13 | - */ | ||
| 14 | -@Getter | ||
| 15 | -@RequiredArgsConstructor | ||
| 16 | -public enum IotModbusFunctionCodeEnum implements ArrayValuable<Integer> { | ||
| 17 | - | ||
| 18 | - READ_COILS(1, "读线圈", true, 5, 15), | ||
| 19 | - READ_DISCRETE_INPUTS(2, "读离散输入", false, null, null), | ||
| 20 | - READ_HOLDING_REGISTERS(3, "读保持寄存器", true, 6, 16), | ||
| 21 | - READ_INPUT_REGISTERS(4, "读输入寄存器", false, null, null); | ||
| 22 | - | ||
| 23 | - public static final Integer[] ARRAYS = Arrays.stream(values()) | ||
| 24 | - .map(IotModbusFunctionCodeEnum::getCode) | ||
| 25 | - .toArray(Integer[]::new); | ||
| 26 | - | ||
| 27 | - /** | ||
| 28 | - * 功能码 | ||
| 29 | - */ | ||
| 30 | - private final Integer code; | ||
| 31 | - /** | ||
| 32 | - * 名称 | ||
| 33 | - */ | ||
| 34 | - private final String name; | ||
| 35 | - /** | ||
| 36 | - * 是否支持写操作 | ||
| 37 | - */ | ||
| 38 | - private final Boolean writable; | ||
| 39 | - /** | ||
| 40 | - * 单个写功能码 | ||
| 41 | - */ | ||
| 42 | - private final Integer writeSingleCode; | ||
| 43 | - /** | ||
| 44 | - * 多个写功能码 | ||
| 45 | - */ | ||
| 46 | - private final Integer writeMultipleCode; | ||
| 47 | - | ||
| 48 | - @Override | ||
| 49 | - public Integer[] array() { | ||
| 50 | - return ARRAYS; | ||
| 51 | - } | ||
| 52 | - | ||
| 53 | - public static IotModbusFunctionCodeEnum valueOf(Integer code) { | ||
| 54 | - return Arrays.stream(values()) | ||
| 55 | - .filter(e -> e.getCode().equals(code)) | ||
| 56 | - .findFirst() | ||
| 57 | - .orElse(null); | ||
| 58 | - } | ||
| 59 | - | ||
| 60 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | ||
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * IoT Modbus 工具类 | ||
| 11 | + * <p> | ||
| 12 | + * 提供: | ||
| 13 | + * 1. Modbus 协议常量(功能码、掩码等) | ||
| 14 | + * 2. CRC-16/MODBUS 计算和校验 | ||
| 15 | + * 3. 功能码分类判断 | ||
| 16 | + * 4. 从解码后的 ${IotModbusFrame} 中提取寄存器值(用于后续的点位翻译) | ||
| 17 | + * | ||
| 18 | + * @author 芋道源码 | ||
| 19 | + */ | ||
| 20 | +@Slf4j | ||
| 21 | +public class IotModbusUtils { | ||
| 22 | + | ||
| 23 | + /** FC01: 读线圈 */ | ||
| 24 | + public static final int FC_READ_COILS = 1; | ||
| 25 | + /** FC02: 读离散输入 */ | ||
| 26 | + public static final int FC_READ_DISCRETE_INPUTS = 2; | ||
| 27 | + /** FC03: 读保持寄存器 */ | ||
| 28 | + public static final int FC_READ_HOLDING_REGISTERS = 3; | ||
| 29 | + /** FC04: 读输入寄存器 */ | ||
| 30 | + public static final int FC_READ_INPUT_REGISTERS = 4; | ||
| 31 | + | ||
| 32 | + /** FC05: 写单个线圈 */ | ||
| 33 | + public static final int FC_WRITE_SINGLE_COIL = 5; | ||
| 34 | + /** FC06: 写单个寄存器 */ | ||
| 35 | + public static final int FC_WRITE_SINGLE_REGISTER = 6; | ||
| 36 | + /** FC15: 写多个线圈 */ | ||
| 37 | + public static final int FC_WRITE_MULTIPLE_COILS = 15; | ||
| 38 | + /** FC16: 写多个寄存器 */ | ||
| 39 | + public static final int FC_WRITE_MULTIPLE_REGISTERS = 16; | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应 | ||
| 43 | + * 例如:请求 FC=0x03,异常响应 FC=0x83(0x03 | 0x80) | ||
| 44 | + */ | ||
| 45 | + public static final int FC_EXCEPTION_MASK = 0x80; | ||
| 46 | + | ||
| 47 | + /** | ||
| 48 | + * 功能码掩码:用于从异常响应中提取原始功能码 | ||
| 49 | + * 例如:异常 FC=0x83,原始 FC = 0x83 & 0x7F = 0x03 | ||
| 50 | + */ | ||
| 51 | + public static final int FC_MASK = 0x7F; | ||
| 52 | + | ||
| 53 | + // ==================== 功能码分类判断 ==================== | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * 判断是否为读响应(FC01-04) | ||
| 57 | + */ | ||
| 58 | + public static boolean isReadResponse(int functionCode) { | ||
| 59 | + return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS; | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + /** | ||
| 63 | + * 判断是否为写响应(FC05/06/15/16) | ||
| 64 | + */ | ||
| 65 | + public static boolean isWriteResponse(int functionCode) { | ||
| 66 | + return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER | ||
| 67 | + || functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS; | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + /** | ||
| 71 | + * 判断是否为异常响应 | ||
| 72 | + */ | ||
| 73 | + public static boolean isExceptionResponse(int functionCode) { | ||
| 74 | + return (functionCode & FC_EXCEPTION_MASK) != 0; | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + /** | ||
| 78 | + * 从异常响应中提取原始功能码 | ||
| 79 | + */ | ||
| 80 | + public static int extractOriginalFunctionCode(int exceptionFunctionCode) { | ||
| 81 | + return exceptionFunctionCode & FC_MASK; | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 判断读功能码是否支持写操作 | ||
| 86 | + * <p> | ||
| 87 | + * FC01(读线圈)和 FC03(读保持寄存器)支持写操作; | ||
| 88 | + * FC02(读离散输入)和 FC04(读输入寄存器)为只读。 | ||
| 89 | + * | ||
| 90 | + * @param readFunctionCode 读功能码(FC01-04) | ||
| 91 | + * @return 是否支持写操作 | ||
| 92 | + */ | ||
| 93 | + @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||
| 94 | + public static boolean isWritable(int readFunctionCode) { | ||
| 95 | + return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS; | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + /** | ||
| 99 | + * 获取单写功能码 | ||
| 100 | + * <p> | ||
| 101 | + * FC01(读线圈)→ FC05(写单个线圈); | ||
| 102 | + * FC03(读保持寄存器)→ FC06(写单个寄存器); | ||
| 103 | + * 其他返回 null(不支持写)。 | ||
| 104 | + * | ||
| 105 | + * @param readFunctionCode 读功能码 | ||
| 106 | + * @return 单写功能码,不支持写时返回 null | ||
| 107 | + */ | ||
| 108 | + @SuppressWarnings("EnhancedSwitchMigration") | ||
| 109 | + public static Integer getWriteSingleFunctionCode(int readFunctionCode) { | ||
| 110 | + switch (readFunctionCode) { | ||
| 111 | + case FC_READ_COILS: | ||
| 112 | + return FC_WRITE_SINGLE_COIL; | ||
| 113 | + case FC_READ_HOLDING_REGISTERS: | ||
| 114 | + return FC_WRITE_SINGLE_REGISTER; | ||
| 115 | + default: | ||
| 116 | + return null; | ||
| 117 | + } | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + /** | ||
| 121 | + * 获取多写功能码 | ||
| 122 | + * <p> | ||
| 123 | + * FC01(读线圈)→ FC15(写多个线圈); | ||
| 124 | + * FC03(读保持寄存器)→ FC16(写多个寄存器); | ||
| 125 | + * 其他返回 null(不支持写)。 | ||
| 126 | + * | ||
| 127 | + * @param readFunctionCode 读功能码 | ||
| 128 | + * @return 多写功能码,不支持写时返回 null | ||
| 129 | + */ | ||
| 130 | + @SuppressWarnings("EnhancedSwitchMigration") | ||
| 131 | + public static Integer getWriteMultipleFunctionCode(int readFunctionCode) { | ||
| 132 | + switch (readFunctionCode) { | ||
| 133 | + case FC_READ_COILS: | ||
| 134 | + return FC_WRITE_MULTIPLE_COILS; | ||
| 135 | + case FC_READ_HOLDING_REGISTERS: | ||
| 136 | + return FC_WRITE_MULTIPLE_REGISTERS; | ||
| 137 | + default: | ||
| 138 | + return null; | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + // ==================== 点位查找 ==================== | ||
| 143 | + | ||
| 144 | + /** | ||
| 145 | + * 查找点位配置 | ||
| 146 | + * | ||
| 147 | + * @param config 设备 Modbus 配置 | ||
| 148 | + * @param identifier 点位标识符 | ||
| 149 | + * @return 匹配的点位配置,未找到返回 null | ||
| 150 | + */ | ||
| 151 | + public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { | ||
| 152 | + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + // ==================== CRC16 工具 ==================== | ||
| 156 | + | ||
| 157 | + /** | ||
| 158 | + * 计算 CRC-16/MODBUS | ||
| 159 | + * | ||
| 160 | + * @param data 数据 | ||
| 161 | + * @param length 计算长度 | ||
| 162 | + * @return CRC16 值 | ||
| 163 | + */ | ||
| 164 | + public static int calculateCrc16(byte[] data, int length) { | ||
| 165 | + int crc = 0xFFFF; | ||
| 166 | + for (int i = 0; i < length; i++) { | ||
| 167 | + crc ^= (data[i] & 0xFF); | ||
| 168 | + for (int j = 0; j < 8; j++) { | ||
| 169 | + if ((crc & 0x0001) != 0) { | ||
| 170 | + crc >>= 1; | ||
| 171 | + crc ^= 0xA001; | ||
| 172 | + } else { | ||
| 173 | + crc >>= 1; | ||
| 174 | + } | ||
| 175 | + } | ||
| 176 | + } | ||
| 177 | + return crc; | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + /** | ||
| 181 | + * 校验 CRC16 | ||
| 182 | + * | ||
| 183 | + * @param data 包含 CRC 的完整数据 | ||
| 184 | + * @return 校验是否通过 | ||
| 185 | + */ | ||
| 186 | + public static boolean verifyCrc16(byte[] data) { | ||
| 187 | + if (data.length < 3) { | ||
| 188 | + return false; | ||
| 189 | + } | ||
| 190 | + int computed = calculateCrc16(data, data.length - 2); | ||
| 191 | + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); | ||
| 192 | + return computed == received; | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + // ==================== 响应值提取 ==================== | ||
| 196 | + | ||
| 197 | + /** | ||
| 198 | + * 从帧中提取寄存器值(FC01-04 读响应) | ||
| 199 | + * | ||
| 200 | + * @param frame 解码后的 Modbus 帧 | ||
| 201 | + * @return 寄存器值数组(int[]),失败返回 null | ||
| 202 | + */ | ||
| 203 | + @SuppressWarnings("EnhancedSwitchMigration") | ||
| 204 | + public static int[] extractValues(IotModbusFrame frame) { | ||
| 205 | + if (frame == null || frame.isException()) { | ||
| 206 | + return null; | ||
| 207 | + } | ||
| 208 | + byte[] pdu = frame.getPdu(); | ||
| 209 | + if (pdu == null || pdu.length < 1) { | ||
| 210 | + return null; | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | + int functionCode = frame.getFunctionCode(); | ||
| 214 | + switch (functionCode) { | ||
| 215 | + case FC_READ_COILS: | ||
| 216 | + case FC_READ_DISCRETE_INPUTS: | ||
| 217 | + return extractCoilValues(pdu); | ||
| 218 | + case FC_READ_HOLDING_REGISTERS: | ||
| 219 | + case FC_READ_INPUT_REGISTERS: | ||
| 220 | + return extractRegisterValues(pdu); | ||
| 221 | + default: | ||
| 222 | + log.warn("[extractValues][不支持的功能码: {}]", functionCode); | ||
| 223 | + return null; | ||
| 224 | + } | ||
| 225 | + } | ||
| 226 | + | ||
| 227 | + /** | ||
| 228 | + * 提取线圈/离散输入值 | ||
| 229 | + * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)] | ||
| 230 | + */ | ||
| 231 | + private static int[] extractCoilValues(byte[] pdu) { | ||
| 232 | + if (pdu.length < 2) { | ||
| 233 | + return null; | ||
| 234 | + } | ||
| 235 | + int byteCount = pdu[0] & 0xFF; | ||
| 236 | + int bitCount = byteCount * 8; | ||
| 237 | + int[] values = new int[bitCount]; | ||
| 238 | + for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) { | ||
| 239 | + values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01); | ||
| 240 | + } | ||
| 241 | + return values; | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + /** | ||
| 245 | + * 提取寄存器值 | ||
| 246 | + * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)] | ||
| 247 | + */ | ||
| 248 | + private static int[] extractRegisterValues(byte[] pdu) { | ||
| 249 | + if (pdu.length < 2) { | ||
| 250 | + return null; | ||
| 251 | + } | ||
| 252 | + int byteCount = pdu[0] & 0xFF; | ||
| 253 | + int registerCount = byteCount / 2; | ||
| 254 | + int[] values = new int[registerCount]; | ||
| 255 | + for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) { | ||
| 256 | + values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF); | ||
| 257 | + } | ||
| 258 | + return values; | ||
| 259 | + } | ||
| 260 | + | ||
| 261 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | 3 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 4 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; | ||
| 5 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; | 4 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; |
| 6 | import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; | 5 | import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; |
| 7 | import com.ghgande.j2mod.modbus.msg.*; | 6 | import com.ghgande.j2mod.modbus.msg.*; |
| @@ -12,6 +11,8 @@ import com.ghgande.j2mod.modbus.util.BitVector; | @@ -12,6 +11,8 @@ import com.ghgande.j2mod.modbus.util.BitVector; | ||
| 12 | import io.vertx.core.Future; | 11 | import io.vertx.core.Future; |
| 13 | import lombok.extern.slf4j.Slf4j; | 12 | import lombok.extern.slf4j.Slf4j; |
| 14 | 13 | ||
| 14 | +import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils.*; | ||
| 15 | + | ||
| 15 | // TODO @AI:感觉它更像一个工具类;但是名字叫 client 很奇怪; | 16 | // TODO @AI:感觉它更像一个工具类;但是名字叫 client 很奇怪; |
| 16 | /** | 17 | /** |
| 17 | * IoT Modbus TCP 客户端 | 18 | * IoT Modbus TCP 客户端 |
| @@ -98,18 +99,14 @@ public class IotModbusTcpClient { | @@ -98,18 +99,14 @@ public class IotModbusTcpClient { | ||
| 98 | */ | 99 | */ |
| 99 | @SuppressWarnings("EnhancedSwitchMigration") | 100 | @SuppressWarnings("EnhancedSwitchMigration") |
| 100 | private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { | 101 | private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { |
| 101 | - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); | ||
| 102 | - if (functionCodeEnum == null) { | ||
| 103 | - throw new IllegalArgumentException("不支持的功能码: " + functionCode); | ||
| 104 | - } | ||
| 105 | - switch (functionCodeEnum) { | ||
| 106 | - case READ_COILS: | 102 | + switch (functionCode) { |
| 103 | + case FC_READ_COILS: | ||
| 107 | return new ReadCoilsRequest(address, count); | 104 | return new ReadCoilsRequest(address, count); |
| 108 | - case READ_DISCRETE_INPUTS: | 105 | + case FC_READ_DISCRETE_INPUTS: |
| 109 | return new ReadInputDiscretesRequest(address, count); | 106 | return new ReadInputDiscretesRequest(address, count); |
| 110 | - case READ_HOLDING_REGISTERS: | 107 | + case FC_READ_HOLDING_REGISTERS: |
| 111 | return new ReadMultipleRegistersRequest(address, count); | 108 | return new ReadMultipleRegistersRequest(address, count); |
| 112 | - case READ_INPUT_REGISTERS: | 109 | + case FC_READ_INPUT_REGISTERS: |
| 113 | return new ReadInputRegistersRequest(address, count); | 110 | return new ReadInputRegistersRequest(address, count); |
| 114 | default: | 111 | default: |
| 115 | throw new IllegalArgumentException("不支持的功能码: " + functionCode); | 112 | throw new IllegalArgumentException("不支持的功能码: " + functionCode); |
| @@ -119,13 +116,10 @@ public class IotModbusTcpClient { | @@ -119,13 +116,10 @@ public class IotModbusTcpClient { | ||
| 119 | /** | 116 | /** |
| 120 | * 创建写入请求 | 117 | * 创建写入请求 |
| 121 | */ | 118 | */ |
| 119 | + @SuppressWarnings("EnhancedSwitchMigration") | ||
| 122 | private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { | 120 | private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { |
| 123 | - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); | ||
| 124 | - if (functionCodeEnum == null) { | ||
| 125 | - throw new IllegalArgumentException("不支持的功能码: " + functionCode); | ||
| 126 | - } | ||
| 127 | - switch (functionCodeEnum) { | ||
| 128 | - case READ_COILS: // 写线圈(使用功能码 5 或 15) | 121 | + switch (functionCode) { |
| 122 | + case FC_READ_COILS: // 写线圈(使用功能码 5 或 15) | ||
| 129 | if (count == 1) { | 123 | if (count == 1) { |
| 130 | return new WriteCoilRequest(address, values[0] != 0); | 124 | return new WriteCoilRequest(address, values[0] != 0); |
| 131 | } else { | 125 | } else { |
| @@ -135,7 +129,7 @@ public class IotModbusTcpClient { | @@ -135,7 +129,7 @@ public class IotModbusTcpClient { | ||
| 135 | } | 129 | } |
| 136 | return new WriteMultipleCoilsRequest(address, bv); | 130 | return new WriteMultipleCoilsRequest(address, bv); |
| 137 | } | 131 | } |
| 138 | - case READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16) | 132 | + case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16) |
| 139 | if (count == 1) { | 133 | if (count == 1) { |
| 140 | return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); | 134 | return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); |
| 141 | } else { | 135 | } else { |
| @@ -145,8 +139,8 @@ public class IotModbusTcpClient { | @@ -145,8 +139,8 @@ public class IotModbusTcpClient { | ||
| 145 | } | 139 | } |
| 146 | return new WriteMultipleRegistersRequest(address, registers); | 140 | return new WriteMultipleRegistersRequest(address, registers); |
| 147 | } | 141 | } |
| 148 | - case READ_DISCRETE_INPUTS: // 只读 | ||
| 149 | - case READ_INPUT_REGISTERS: // 只读 | 142 | + case FC_READ_DISCRETE_INPUTS: // 只读 |
| 143 | + case FC_READ_INPUT_REGISTERS: // 只读 | ||
| 150 | return null; | 144 | return null; |
| 151 | default: | 145 | default: |
| 152 | throw new IllegalArgumentException("不支持的功能码: " + functionCode); | 146 | throw new IllegalArgumentException("不支持的功能码: " + functionCode); |
| @@ -156,13 +150,10 @@ public class IotModbusTcpClient { | @@ -156,13 +150,10 @@ public class IotModbusTcpClient { | ||
| 156 | /** | 150 | /** |
| 157 | * 从响应中提取值 | 151 | * 从响应中提取值 |
| 158 | */ | 152 | */ |
| 153 | + @SuppressWarnings("EnhancedSwitchMigration") | ||
| 159 | private int[] extractValues(ModbusResponse response, Integer functionCode) { | 154 | private int[] extractValues(ModbusResponse response, Integer functionCode) { |
| 160 | - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); | ||
| 161 | - if (functionCodeEnum == null) { | ||
| 162 | - throw new IllegalArgumentException("不支持的功能码: " + functionCode); | ||
| 163 | - } | ||
| 164 | - switch (functionCodeEnum) { | ||
| 165 | - case READ_COILS: | 155 | + switch (functionCode) { |
| 156 | + case FC_READ_COILS: | ||
| 166 | ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; | 157 | ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; |
| 167 | int bitCount = coilsResponse.getBitCount(); | 158 | int bitCount = coilsResponse.getBitCount(); |
| 168 | int[] coilValues = new int[bitCount]; | 159 | int[] coilValues = new int[bitCount]; |
| @@ -170,7 +161,7 @@ public class IotModbusTcpClient { | @@ -170,7 +161,7 @@ public class IotModbusTcpClient { | ||
| 170 | coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0; | 161 | coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0; |
| 171 | } | 162 | } |
| 172 | return coilValues; | 163 | return coilValues; |
| 173 | - case READ_DISCRETE_INPUTS: | 164 | + case FC_READ_DISCRETE_INPUTS: |
| 174 | ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response; | 165 | ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response; |
| 175 | int discreteCount = discretesResponse.getBitCount(); | 166 | int discreteCount = discretesResponse.getBitCount(); |
| 176 | int[] discreteValues = new int[discreteCount]; | 167 | int[] discreteValues = new int[discreteCount]; |
| @@ -178,7 +169,7 @@ public class IotModbusTcpClient { | @@ -178,7 +169,7 @@ public class IotModbusTcpClient { | ||
| 178 | discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0; | 169 | discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0; |
| 179 | } | 170 | } |
| 180 | return discreteValues; | 171 | return discreteValues; |
| 181 | - case READ_HOLDING_REGISTERS: | 172 | + case FC_READ_HOLDING_REGISTERS: |
| 182 | ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response; | 173 | ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response; |
| 183 | InputRegister[] holdingRegisters = holdingResponse.getRegisters(); | 174 | InputRegister[] holdingRegisters = holdingResponse.getRegisters(); |
| 184 | int[] holdingValues = new int[holdingRegisters.length]; | 175 | int[] holdingValues = new int[holdingRegisters.length]; |
| @@ -186,7 +177,7 @@ public class IotModbusTcpClient { | @@ -186,7 +177,7 @@ public class IotModbusTcpClient { | ||
| 186 | holdingValues[i] = holdingRegisters[i].getValue(); | 177 | holdingValues[i] = holdingRegisters[i].getValue(); |
| 187 | } | 178 | } |
| 188 | return holdingValues; | 179 | return holdingValues; |
| 189 | - case READ_INPUT_REGISTERS: | 180 | + case FC_READ_INPUT_REGISTERS: |
| 190 | ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response; | 181 | ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response; |
| 191 | InputRegister[] inputRegisters = inputResponse.getRegisters(); | 182 | InputRegister[] inputRegisters = inputResponse.getRegisters(); |
| 192 | int[] inputValues = new int[inputRegisters.length]; | 183 | int[] inputValues = new int[inputRegisters.length]; |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.collection.CollUtil; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | 3 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; | ||
| 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; | ||
| 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; |
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; |
| 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; |
| @@ -60,19 +59,19 @@ public class IotModbusTcpDownstreamHandler { | @@ -60,19 +59,19 @@ public class IotModbusTcpDownstreamHandler { | ||
| 60 | for (Map.Entry<String, Object> entry : propertyMap.entrySet()) { | 59 | for (Map.Entry<String, Object> entry : propertyMap.entrySet()) { |
| 61 | String identifier = entry.getKey(); | 60 | String identifier = entry.getKey(); |
| 62 | Object value = entry.getValue(); | 61 | Object value = entry.getValue(); |
| 63 | - // 2.1.1 查找对应的点位配置 | ||
| 64 | - IotModbusPointRespDTO point = findPoint(config, identifier); | 62 | + // 2.1 查找对应的点位配置 |
| 63 | + IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier); | ||
| 65 | if (point == null) { | 64 | if (point == null) { |
| 66 | log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); | 65 | log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); |
| 67 | continue; | 66 | continue; |
| 68 | } | 67 | } |
| 69 | - // 2.1.2 检查是否支持写操作 | ||
| 70 | - if (!isWritable(point.getFunctionCode())) { | 68 | + // 2.2 检查是否支持写操作 |
| 69 | + if (!IotModbusUtils.isWritable(point.getFunctionCode())) { | ||
| 71 | log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); | 70 | log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); |
| 72 | continue; | 71 | continue; |
| 73 | } | 72 | } |
| 74 | 73 | ||
| 75 | - // 2.2 执行写入 | 74 | + // 2.3 执行写入 |
| 76 | writeProperty(config, point, value); | 75 | writeProperty(config, point, value); |
| 77 | } | 76 | } |
| 78 | } | 77 | } |
| @@ -104,19 +103,4 @@ public class IotModbusTcpDownstreamHandler { | @@ -104,19 +103,4 @@ public class IotModbusTcpDownstreamHandler { | ||
| 104 | config.getDeviceId(), point.getIdentifier(), e)); | 103 | config.getDeviceId(), point.getIdentifier(), e)); |
| 105 | } | 104 | } |
| 106 | 105 | ||
| 107 | - /** | ||
| 108 | - * 查找点位配置 | ||
| 109 | - */ | ||
| 110 | - private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { | ||
| 111 | - return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); | ||
| 112 | - } | ||
| 113 | - | ||
| 114 | - /** | ||
| 115 | - * 检查功能码是否支持写操作 | ||
| 116 | - */ | ||
| 117 | - private boolean isWritable(Integer functionCode) { | ||
| 118 | - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); | ||
| 119 | - return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable()); | ||
| 120 | - } | ||
| 121 | - | ||
| 122 | } | 106 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; |
| 8 | -import lombok.RequiredArgsConstructor; | ||
| 9 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 10 | 8 | ||
| 11 | -// TODO @AI:是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java | ||
| 12 | /** | 9 | /** |
| 13 | * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 | 10 | * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 |
| 14 | * | 11 | * |
| 15 | * @author 芋道源码 | 12 | * @author 芋道源码 |
| 16 | */ | 13 | */ |
| 17 | -@RequiredArgsConstructor | ||
| 18 | @Slf4j | 14 | @Slf4j |
| 19 | -public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | 15 | +public class IotModbusTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber { |
| 20 | 16 | ||
| 21 | - private final IotModbusTcpMasterProtocol protocol; | ||
| 22 | private final IotModbusTcpDownstreamHandler downstreamHandler; | 17 | private final IotModbusTcpDownstreamHandler downstreamHandler; |
| 23 | - private final IotMessageBus messageBus; | ||
| 24 | 18 | ||
| 25 | - /** | ||
| 26 | - * 启动订阅 | ||
| 27 | - */ | ||
| 28 | - public void start() { | ||
| 29 | - messageBus.register(this); | ||
| 30 | - log.info("[start][Modbus TCP Master 下行消息订阅器已启动, topic={}]", getTopic()); | ||
| 31 | - } | ||
| 32 | - | ||
| 33 | - /** | ||
| 34 | - * 停止订阅 | ||
| 35 | - */ | ||
| 36 | - public void stop() { | ||
| 37 | - messageBus.unregister(this); | ||
| 38 | - log.info("[stop][Modbus TCP Master 下行消息订阅器已停止]"); | ||
| 39 | - } | ||
| 40 | - | ||
| 41 | - @Override | ||
| 42 | - public String getTopic() { | ||
| 43 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); | ||
| 44 | - } | ||
| 45 | - | ||
| 46 | - @Override | ||
| 47 | - public String getGroup() { | ||
| 48 | - return getTopic(); // 点对点消费 | 19 | + public IotModbusTcpDownstreamSubscriber(IotModbusTcpMasterProtocol protocol, |
| 20 | + IotModbusTcpDownstreamHandler downstreamHandler, | ||
| 21 | + IotMessageBus messageBus) { | ||
| 22 | + super(protocol, messageBus); | ||
| 23 | + this.downstreamHandler = downstreamHandler; | ||
| 49 | } | 24 | } |
| 50 | 25 | ||
| 51 | @Override | 26 | @Override |
| 52 | - public void onMessage(IotDeviceMessage message) { | ||
| 53 | - log.debug("[onMessage][收到下行消息: {}]", message); | ||
| 54 | - try { | ||
| 55 | - downstreamHandler.handle(message); | ||
| 56 | - } catch (Exception e) { | ||
| 57 | - log.error("[onMessage][处理下行消息失败]", e); | ||
| 58 | - } | 27 | + protected void handleMessage(IotDeviceMessage message) { |
| 28 | + downstreamHandler.handle(message); | ||
| 59 | } | 29 | } |
| 60 | 30 | ||
| 61 | } | 31 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; |
| 2 | 2 | ||
| 3 | +import jakarta.validation.constraints.Max; | ||
| 3 | import jakarta.validation.constraints.Min; | 4 | import jakarta.validation.constraints.Min; |
| 4 | import jakarta.validation.constraints.NotNull; | 5 | import jakarta.validation.constraints.NotNull; |
| 5 | import lombok.Data; | 6 | import lombok.Data; |
| @@ -25,7 +26,7 @@ public class IotModbusTcpSlaveConfig { | @@ -25,7 +26,7 @@ public class IotModbusTcpSlaveConfig { | ||
| 25 | */ | 26 | */ |
| 26 | @NotNull(message = "自定义功能码不能为空") | 27 | @NotNull(message = "自定义功能码不能为空") |
| 27 | @Min(value = 65, message = "自定义功能码不能小于 65") | 28 | @Min(value = 65, message = "自定义功能码不能小于 65") |
| 28 | - // TODO @AI:搞个范围; | 29 | + @Max(value = 72, message = "自定义功能码不能大于 72") |
| 29 | private Integer customFunctionCode = 65; | 30 | private Integer customFunctionCode = 65; |
| 30 | 31 | ||
| 31 | /** | 32 | /** |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | ||
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; | ||
| 3 | import lombok.Data; | 5 | import lombok.Data; |
| 4 | import lombok.experimental.Accessors; | 6 | import lombok.experimental.Accessors; |
| 5 | 7 | ||
| @@ -25,19 +27,19 @@ public class IotModbusFrame { | @@ -25,19 +27,19 @@ public class IotModbusFrame { | ||
| 25 | */ | 27 | */ |
| 26 | private byte[] pdu; | 28 | private byte[] pdu; |
| 27 | /** | 29 | /** |
| 28 | - * 事务标识符(TCP 模式特有) | ||
| 29 | - * | ||
| 30 | - * // TODO @AI:最好是 @某个类型独有; | 30 | + * 事务标识符 |
| 31 | + * <p> | ||
| 32 | + * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值, | ||
| 31 | */ | 33 | */ |
| 32 | private Integer transactionId; | 34 | private Integer transactionId; |
| 33 | 35 | ||
| 34 | /** | 36 | /** |
| 35 | - * 是否异常响应 | ||
| 36 | - */ | ||
| 37 | - private boolean exception; | ||
| 38 | - // TODO @AI:是不是要枚举一些异常;另外,是不是覆盖掉 exception;因为只要判断有异常码是不是就可以了; | ||
| 39 | - /** | ||
| 40 | - * 异常码(当 exception=true 时有效) | 37 | + * 异常码 |
| 38 | + * <p> | ||
| 39 | + * 当功能码最高位为 1 时(异常响应),此字段存储异常码。 | ||
| 40 | + * 为 null 表示非异常响应。 | ||
| 41 | + * | ||
| 42 | + * @see IotModbusUtils#FC_EXCEPTION_MASK | ||
| 41 | */ | 43 | */ |
| 42 | private Integer exceptionCode; | 44 | private Integer exceptionCode; |
| 43 | 45 | ||
| @@ -46,4 +48,11 @@ public class IotModbusFrame { | @@ -46,4 +48,11 @@ public class IotModbusFrame { | ||
| 46 | */ | 48 | */ |
| 47 | private String customData; | 49 | private String customData; |
| 48 | 50 | ||
| 51 | + /** | ||
| 52 | + * 是否异常响应(基于 exceptionCode 是否有值判断) | ||
| 53 | + */ | ||
| 54 | + public boolean isException() { | ||
| 55 | + return exceptionCode != null; | ||
| 56 | + } | ||
| 57 | + | ||
| 49 | } | 58 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; | ||
| 4 | import io.vertx.core.Handler; | 5 | import io.vertx.core.Handler; |
| 5 | import io.vertx.core.buffer.Buffer; | 6 | import io.vertx.core.buffer.Buffer; |
| 6 | import io.vertx.core.parsetools.RecordParser; | 7 | import io.vertx.core.parsetools.RecordParser; |
| @@ -118,11 +119,8 @@ public class IotModbusFrameDecoder { | @@ -118,11 +119,8 @@ public class IotModbusFrameDecoder { | ||
| 118 | .setPdu(pdu) | 119 | .setPdu(pdu) |
| 119 | .setTransactionId(transactionId); | 120 | .setTransactionId(transactionId); |
| 120 | // 异常响应 | 121 | // 异常响应 |
| 121 | - // TODO @AI:0x80 看看是不是要枚举; | ||
| 122 | - if ((functionCode & 0x80) != 0) { | ||
| 123 | - frame.setException(true); | ||
| 124 | - // TODO @AI:0x7f 看看是不是要枚举; | ||
| 125 | - frame.setFunctionCode(functionCode & 0x7F); | 122 | + if (IotModbusUtils.isExceptionResponse(functionCode)) { |
| 123 | + frame.setFunctionCode(IotModbusUtils.extractOriginalFunctionCode(functionCode)); | ||
| 126 | if (pdu.length >= 1) { | 124 | if (pdu.length >= 1) { |
| 127 | frame.setExceptionCode(pdu[0] & 0xFF); | 125 | frame.setExceptionCode(pdu[0] & 0xFF); |
| 128 | } | 126 | } |
| @@ -247,7 +245,7 @@ public class IotModbusFrameDecoder { | @@ -247,7 +245,7 @@ public class IotModbusFrameDecoder { | ||
| 247 | * 状态机流程: | 245 | * 状态机流程: |
| 248 | * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode | 246 | * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode |
| 249 | * Phase 2: 根据 functionCode 确定剩余长度: | 247 | * Phase 2: 根据 functionCode 确定剩余长度: |
| 250 | - * - 异常响应 (FC & 0x80):fixedSizeMode(3) → exceptionCode(1) + CRC(2) | 248 | + * - 异常响应 (FC & EXCEPTION_MASK):fixedSizeMode(3) → exceptionCode(1) + CRC(2) |
| 251 | * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) | 249 | * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) |
| 252 | * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) | 250 | * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) |
| 253 | * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) | 251 | * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) |
| @@ -283,7 +281,7 @@ public class IotModbusFrameDecoder { | @@ -283,7 +281,7 @@ public class IotModbusFrameDecoder { | ||
| 283 | this.slaveId = bytes[0]; | 281 | this.slaveId = bytes[0]; |
| 284 | this.functionCode = bytes[1]; | 282 | this.functionCode = bytes[1]; |
| 285 | int fc = functionCode & 0xFF; | 283 | int fc = functionCode & 0xFF; |
| 286 | - if ((fc & 0x80) != 0) { | 284 | + if (IotModbusUtils.isExceptionResponse(fc)) { |
| 287 | // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节 | 285 | // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节 |
| 288 | // 已有 6 字节(多 1 字节),取前 5 字节组装 | 286 | // 已有 6 字节(多 1 字节),取前 5 字节组装 |
| 289 | Buffer frame = Buffer.buffer(5); | 287 | Buffer frame = Buffer.buffer(5); |
| @@ -292,7 +290,7 @@ public class IotModbusFrameDecoder { | @@ -292,7 +290,7 @@ public class IotModbusFrameDecoder { | ||
| 292 | frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC | 290 | frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC |
| 293 | emitFrame(frame); | 291 | emitFrame(frame); |
| 294 | resetToHeader(); | 292 | resetToHeader(); |
| 295 | - } else if (isReadResponse(fc) || fc == customFunctionCode) { | 293 | + } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) { |
| 296 | // 读响应或自定义 FC:bytes[2] = byteCount | 294 | // 读响应或自定义 FC:bytes[2] = byteCount |
| 297 | this.byteCount = bytes[2]; | 295 | this.byteCount = bytes[2]; |
| 298 | int bc = byteCount & 0xFF; | 296 | int bc = byteCount & 0xFF; |
| @@ -317,7 +315,7 @@ public class IotModbusFrameDecoder { | @@ -317,7 +315,7 @@ public class IotModbusFrameDecoder { | ||
| 317 | this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC | 315 | this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC |
| 318 | parser.fixedSizeMode(remaining); | 316 | parser.fixedSizeMode(remaining); |
| 319 | } | 317 | } |
| 320 | - } else if (isWriteResponse(fc)) { | 318 | + } else if (IotModbusUtils.isWriteResponse(fc)) { |
| 321 | // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节 | 319 | // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节 |
| 322 | // 已有 6 字节,还需 2 字节 | 320 | // 已有 6 字节,还需 2 字节 |
| 323 | state = STATE_WRITE_BODY; | 321 | state = STATE_WRITE_BODY; |
| @@ -358,15 +356,15 @@ public class IotModbusFrameDecoder { | @@ -358,15 +356,15 @@ public class IotModbusFrameDecoder { | ||
| 358 | this.slaveId = header[0]; | 356 | this.slaveId = header[0]; |
| 359 | this.functionCode = header[1]; | 357 | this.functionCode = header[1]; |
| 360 | int fc = functionCode & 0xFF; | 358 | int fc = functionCode & 0xFF; |
| 361 | - if ((fc & 0x80) != 0) { | 359 | + if (IotModbusUtils.isExceptionResponse(fc)) { |
| 362 | // 异常响应 | 360 | // 异常响应 |
| 363 | state = STATE_EXCEPTION_BODY; | 361 | state = STATE_EXCEPTION_BODY; |
| 364 | parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) | 362 | parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) |
| 365 | - } else if (isReadResponse(fc) || fc == customFunctionCode) { | 363 | + } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) { |
| 366 | // 读响应或自定义 FC | 364 | // 读响应或自定义 FC |
| 367 | state = STATE_READ_BYTE_COUNT; | 365 | state = STATE_READ_BYTE_COUNT; |
| 368 | parser.fixedSizeMode(1); // byteCount | 366 | parser.fixedSizeMode(1); // byteCount |
| 369 | - } else if (isWriteResponse(fc)) { | 367 | + } else if (IotModbusUtils.isWriteResponse(fc)) { |
| 370 | // 写响应 | 368 | // 写响应 |
| 371 | state = STATE_WRITE_BODY; | 369 | state = STATE_WRITE_BODY; |
| 372 | pendingData = Buffer.buffer(); | 370 | pendingData = Buffer.buffer(); |
| @@ -438,14 +436,6 @@ public class IotModbusFrameDecoder { | @@ -438,14 +436,6 @@ public class IotModbusFrameDecoder { | ||
| 438 | parser.fixedSizeMode(2); // slaveId + FC | 436 | parser.fixedSizeMode(2); // slaveId + FC |
| 439 | } | 437 | } |
| 440 | 438 | ||
| 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 | } | 439 | } |
| 450 | 440 | ||
| 451 | } | 441 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | 3 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; | ||
| 4 | import lombok.RequiredArgsConstructor; | 5 | import lombok.RequiredArgsConstructor; |
| 5 | import lombok.extern.slf4j.Slf4j; | 6 | import lombok.extern.slf4j.Slf4j; |
| 6 | 7 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; | ||
| 2 | - | ||
| 3 | -import lombok.extern.slf4j.Slf4j; | ||
| 4 | - | ||
| 5 | -/** | ||
| 6 | - * IoT Modbus 工具类 | ||
| 7 | - * <p> | ||
| 8 | - * 提供: | ||
| 9 | - * 1. CRC-16/MODBUS 计算和校验 | ||
| 10 | - * 2. 从解码后的 IotModbusFrame 中提取寄存器值(用于后续的点位翻译) | ||
| 11 | - * | ||
| 12 | - * @author 芋道源码 | ||
| 13 | - */ | ||
| 14 | -@Slf4j | ||
| 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 | - // ==================== 响应值提取 ==================== | ||
| 61 | - | ||
| 62 | - /** | ||
| 63 | - * 从帧中提取寄存器值(FC01-04 读响应) | ||
| 64 | - * | ||
| 65 | - * @param frame 解码后的 Modbus 帧 | ||
| 66 | - * @return 寄存器值数组(int[]),失败返回 null | ||
| 67 | - */ | ||
| 68 | - @SuppressWarnings("EnhancedSwitchMigration") | ||
| 69 | - public static int[] extractValues(IotModbusFrame frame) { | ||
| 70 | - if (frame == null || frame.isException()) { | ||
| 71 | - return null; | ||
| 72 | - } | ||
| 73 | - byte[] pdu = frame.getPdu(); | ||
| 74 | - if (pdu == null || pdu.length < 1) { | ||
| 75 | - return null; | ||
| 76 | - } | ||
| 77 | - | ||
| 78 | - // TODO @AI:jmodbus 看看有没可以复用的枚举类 | ||
| 79 | - int functionCode = frame.getFunctionCode(); | ||
| 80 | - switch (functionCode) { | ||
| 81 | - case 1: // Read Coils | ||
| 82 | - case 2: // Read Discrete Inputs | ||
| 83 | - return extractCoilValues(pdu); | ||
| 84 | - case 3: // Read Holding Registers | ||
| 85 | - case 4: // Read Input Registers | ||
| 86 | - return extractRegisterValues(pdu); | ||
| 87 | - default: | ||
| 88 | - log.warn("[extractValues][不支持的功能码: {}]", functionCode); | ||
| 89 | - return null; | ||
| 90 | - } | ||
| 91 | - } | ||
| 92 | - | ||
| 93 | - /** | ||
| 94 | - * 提取线圈/离散输入值 | ||
| 95 | - * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)] | ||
| 96 | - */ | ||
| 97 | - private static int[] extractCoilValues(byte[] pdu) { | ||
| 98 | - if (pdu.length < 2) { | ||
| 99 | - return null; | ||
| 100 | - } | ||
| 101 | - int byteCount = pdu[0] & 0xFF; | ||
| 102 | - int bitCount = byteCount * 8; | ||
| 103 | - int[] values = new int[bitCount]; | ||
| 104 | - for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) { | ||
| 105 | - values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01); | ||
| 106 | - } | ||
| 107 | - return values; | ||
| 108 | - } | ||
| 109 | - | ||
| 110 | - /** | ||
| 111 | - * 提取寄存器值 | ||
| 112 | - * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)] | ||
| 113 | - */ | ||
| 114 | - private static int[] extractRegisterValues(byte[] pdu) { | ||
| 115 | - if (pdu.length < 2) { | ||
| 116 | - return null; | ||
| 117 | - } | ||
| 118 | - int byteCount = pdu[0] & 0xFF; | ||
| 119 | - int registerCount = byteCount / 2; | ||
| 120 | - int[] values = new int[registerCount]; | ||
| 121 | - for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) { | ||
| 122 | - values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF); | ||
| 123 | - } | ||
| 124 | - return values; | ||
| 125 | - } | ||
| 126 | - | ||
| 127 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.collection.CollUtil; | 3 | +import cn.hutool.core.lang.Assert; |
| 4 | +import cn.hutool.core.util.ObjUtil; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | 8 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 8 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum; | ||
| 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; |
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; | ||
| 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; |
| 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; |
| @@ -18,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; | @@ -18,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; | ||
| 18 | import java.util.Map; | 19 | import java.util.Map; |
| 19 | import java.util.concurrent.atomic.AtomicInteger; | 20 | import java.util.concurrent.atomic.AtomicInteger; |
| 20 | 21 | ||
| 21 | -// TODO @AI:看看能不能和 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java 有一些复用逻辑; | ||
| 22 | /** | 22 | /** |
| 23 | * IoT Modbus TCP Slave 下行消息处理器 | 23 | * IoT Modbus TCP Slave 下行消息处理器 |
| 24 | * <p> | 24 | * <p> |
| @@ -48,7 +48,7 @@ public class IotModbusTcpSlaveDownstreamHandler { | @@ -48,7 +48,7 @@ public class IotModbusTcpSlaveDownstreamHandler { | ||
| 48 | @SuppressWarnings("unchecked") | 48 | @SuppressWarnings("unchecked") |
| 49 | public void handle(IotDeviceMessage message) { | 49 | public void handle(IotDeviceMessage message) { |
| 50 | // 1.1 检查是否是属性设置消息 | 50 | // 1.1 检查是否是属性设置消息 |
| 51 | - if (!IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod().equals(message.getMethod())) { | 51 | + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) { |
| 52 | log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); | 52 | log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); |
| 53 | return; | 53 | return; |
| 54 | } | 54 | } |
| @@ -76,13 +76,13 @@ public class IotModbusTcpSlaveDownstreamHandler { | @@ -76,13 +76,13 @@ public class IotModbusTcpSlaveDownstreamHandler { | ||
| 76 | String identifier = entry.getKey(); | 76 | String identifier = entry.getKey(); |
| 77 | Object value = entry.getValue(); | 77 | Object value = entry.getValue(); |
| 78 | // 2.1 查找对应的点位配置 | 78 | // 2.1 查找对应的点位配置 |
| 79 | - IotModbusPointRespDTO point = findPoint(config, identifier); | 79 | + IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier); |
| 80 | if (point == null) { | 80 | if (point == null) { |
| 81 | log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); | 81 | log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); |
| 82 | continue; | 82 | continue; |
| 83 | } | 83 | } |
| 84 | // 2.2 检查是否支持写操作 | 84 | // 2.2 检查是否支持写操作 |
| 85 | - if (!isWritable(point.getFunctionCode())) { | 85 | + if (!IotModbusUtils.isWritable(point.getFunctionCode())) { |
| 86 | log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); | 86 | log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); |
| 87 | continue; | 87 | continue; |
| 88 | } | 88 | } |
| @@ -97,56 +97,36 @@ public class IotModbusTcpSlaveDownstreamHandler { | @@ -97,56 +97,36 @@ public class IotModbusTcpSlaveDownstreamHandler { | ||
| 97 | */ | 97 | */ |
| 98 | private void writeProperty(Long deviceId, ConnectionInfo connInfo, | 98 | private void writeProperty(Long deviceId, ConnectionInfo connInfo, |
| 99 | IotModbusPointRespDTO point, Object value) { | 99 | IotModbusPointRespDTO point, Object value) { |
| 100 | - // 1. 转换属性值为原始值 | 100 | + // 1.1 转换属性值为原始值 |
| 101 | int[] rawValues = dataConverter.convertToRawValues(value, point); | 101 | int[] rawValues = dataConverter.convertToRawValues(value, point); |
| 102 | 102 | ||
| 103 | - // 2. 确定帧格式和事务 ID | 103 | + // 1.2 确定帧格式和事务 ID |
| 104 | IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); | 104 | IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); |
| 105 | - if (frameFormat == null) { | ||
| 106 | - frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP; | ||
| 107 | - } | 105 | + Assert.notNull(frameFormat, "连接帧格式不能为空"); |
| 108 | int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; | 106 | int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; |
| 109 | int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; | 107 | int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; |
| 110 | - | ||
| 111 | - // 3. 编码写请求 | 108 | + // 1.3 编码写请求 |
| 112 | byte[] data; | 109 | byte[] data; |
| 113 | - IotModbusFunctionCodeEnum fcEnum = IotModbusFunctionCodeEnum.valueOf(point.getFunctionCode()); | ||
| 114 | - if (fcEnum == null) { | ||
| 115 | - log.warn("[writeProperty][未知功能码: {}]", point.getFunctionCode()); | ||
| 116 | - return; | ||
| 117 | - } | ||
| 118 | - if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) { | 110 | + int readFunctionCode = point.getFunctionCode(); |
| 111 | + Integer writeSingleCode = IotModbusUtils.getWriteSingleFunctionCode(readFunctionCode); | ||
| 112 | + Integer writeMultipleCode = IotModbusUtils.getWriteMultipleFunctionCode(readFunctionCode); | ||
| 113 | + if (rawValues.length == 1 && writeSingleCode != null) { | ||
| 119 | // 单个值:使用单写功能码(FC05/FC06) | 114 | // 单个值:使用单写功能码(FC05/FC06) |
| 120 | - data = frameEncoder.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), | 115 | + data = frameEncoder.encodeWriteSingleRequest(slaveId, writeSingleCode, |
| 121 | point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); | 116 | point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); |
| 122 | - } else if (fcEnum.getWriteMultipleCode() != null) { | 117 | + } else if (writeMultipleCode != null) { |
| 123 | // 多个值:使用多写功能码(FC15/FC16) | 118 | // 多个值:使用多写功能码(FC15/FC16) |
| 124 | data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, | 119 | data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, |
| 125 | point.getRegisterAddress(), rawValues, frameFormat, transactionId); | 120 | point.getRegisterAddress(), rawValues, frameFormat, transactionId); |
| 126 | } else { | 121 | } else { |
| 127 | - log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier()); | 122 | + log.warn("[writeProperty][点位 {} 不支持写操作, 功能码={}]", point.getIdentifier(), readFunctionCode); |
| 128 | return; | 123 | return; |
| 129 | } | 124 | } |
| 130 | 125 | ||
| 131 | - // 4. 发送 | 126 | + // 2. 发送 |
| 132 | connectionManager.sendToDevice(deviceId, data); | 127 | connectionManager.sendToDevice(deviceId, data); |
| 133 | log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", | 128 | log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", |
| 134 | deviceId, point.getIdentifier(), value); | 129 | deviceId, point.getIdentifier(), value); |
| 135 | } | 130 | } |
| 136 | 131 | ||
| 137 | - /** | ||
| 138 | - * 查找点位配置 | ||
| 139 | - */ | ||
| 140 | - private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { | ||
| 141 | - return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); | ||
| 142 | - } | ||
| 143 | - | ||
| 144 | - /** | ||
| 145 | - * 检查功能码是否支持写操作 | ||
| 146 | - */ | ||
| 147 | - private boolean isWritable(Integer functionCode) { | ||
| 148 | - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode); | ||
| 149 | - return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable()); | ||
| 150 | - } | ||
| 151 | - | ||
| 152 | } | 132 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 3 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | ||
| 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; |
| 8 | -import lombok.RequiredArgsConstructor; | ||
| 9 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 10 | 8 | ||
| 11 | -// TODO @AI:是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java | ||
| 12 | /** | 9 | /** |
| 13 | * IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 | 10 | * IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 |
| 14 | * | 11 | * |
| 15 | * @author 芋道源码 | 12 | * @author 芋道源码 |
| 16 | */ | 13 | */ |
| 17 | -@RequiredArgsConstructor | ||
| 18 | @Slf4j | 14 | @Slf4j |
| 19 | -public class IotModbusTcpSlaveDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | 15 | +public class IotModbusTcpSlaveDownstreamSubscriber extends IotProtocolDownstreamSubscriber { |
| 20 | 16 | ||
| 21 | - private final IotModbusTcpSlaveProtocol protocol; | ||
| 22 | private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; | 17 | private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; |
| 23 | - private final IotMessageBus messageBus; | ||
| 24 | 18 | ||
| 25 | - /** | ||
| 26 | - * 启动订阅 | ||
| 27 | - */ | ||
| 28 | - public void start() { | ||
| 29 | - messageBus.register(this); | ||
| 30 | - log.info("[start][Modbus TCP Slave 下行消息订阅器已启动, topic={}]", getTopic()); | ||
| 31 | - } | ||
| 32 | - | ||
| 33 | - /** | ||
| 34 | - * 停止订阅 | ||
| 35 | - */ | ||
| 36 | - public void stop() { | ||
| 37 | - messageBus.unregister(this); | ||
| 38 | - log.info("[stop][Modbus TCP Slave 下行消息订阅器已停止]"); | ||
| 39 | - } | ||
| 40 | - | ||
| 41 | - @Override | ||
| 42 | - public String getTopic() { | ||
| 43 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); | ||
| 44 | - } | ||
| 45 | - | ||
| 46 | - @Override | ||
| 47 | - public String getGroup() { | ||
| 48 | - return getTopic(); // 点对点消费 | 19 | + public IotModbusTcpSlaveDownstreamSubscriber(IotModbusTcpSlaveProtocol protocol, |
| 20 | + IotModbusTcpSlaveDownstreamHandler downstreamHandler, | ||
| 21 | + IotMessageBus messageBus) { | ||
| 22 | + super(protocol, messageBus); | ||
| 23 | + this.downstreamHandler = downstreamHandler; | ||
| 49 | } | 24 | } |
| 50 | 25 | ||
| 51 | @Override | 26 | @Override |
| 52 | - public void onMessage(IotDeviceMessage message) { | ||
| 53 | - log.debug("[onMessage][收到下行消息: {}]", message); | ||
| 54 | - try { | ||
| 55 | - downstreamHandler.handle(message); | ||
| 56 | - } catch (Exception e) { | ||
| 57 | - log.error("[onMessage][处理下行消息失败]", e); | ||
| 58 | - } | 27 | + protected void handleMessage(IotDeviceMessage message) { |
| 28 | + downstreamHandler.handle(message); | ||
| 59 | } | 29 | } |
| 60 | 30 | ||
| 61 | } | 31 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java
| @@ -5,14 +5,15 @@ import cn.hutool.core.lang.Assert; | @@ -5,14 +5,15 @@ import cn.hutool.core.lang.Assert; | ||
| 5 | import cn.hutool.core.map.MapUtil; | 5 | import cn.hutool.core.map.MapUtil; |
| 6 | import cn.hutool.core.util.BooleanUtil; | 6 | import cn.hutool.core.util.BooleanUtil; |
| 7 | import cn.hutool.core.util.StrUtil; | 7 | import cn.hutool.core.util.StrUtil; |
| 8 | -import cn.hutool.json.JSONObject; | ||
| 9 | -import cn.hutool.json.JSONUtil; | ||
| 10 | import cn.iocoder.yudao.framework.common.exception.ServiceException; | 8 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 9 | +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | ||
| 11 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 10 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 11 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 12 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 12 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 13 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 13 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 14 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | 14 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 15 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; | 15 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 16 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | ||
| 16 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 17 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 17 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; | 18 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 18 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; | 19 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; |
| @@ -20,9 +21,9 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | @@ -20,9 +21,9 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 20 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 21 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 21 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 22 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 22 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | 23 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; |
| 24 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; | ||
| 23 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; | 25 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; |
| 24 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; | 26 | 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; | ||
| 26 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; | 27 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; |
| 27 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; | 28 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; |
| 28 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; | 29 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| @@ -40,14 +41,12 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC | @@ -40,14 +41,12 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC | ||
| 40 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | 41 | 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 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
| 42 | 43 | ||
| 43 | -// DONE @AI:逻辑有点多,看看是不是分区域! => 已按区域划分:认证 / 轮询响应 | ||
| 44 | /** | 44 | /** |
| 45 | * IoT Modbus TCP Slave 上行数据处理器 | 45 | * IoT Modbus TCP Slave 上行数据处理器 |
| 46 | * <p> | 46 | * <p> |
| 47 | * 处理: | 47 | * 处理: |
| 48 | * 1. 自定义 FC 认证 | 48 | * 1. 自定义 FC 认证 |
| 49 | - * 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post | ||
| 50 | - * // DONE @AI:不用主动上报;主动上报走标准 tcp 即可 | 49 | + * 2. 轮询响应 → 点位翻译 → thing.property.post |
| 51 | * | 50 | * |
| 52 | * @author 芋道源码 | 51 | * @author 芋道源码 |
| 53 | */ | 52 | */ |
| @@ -105,29 +104,14 @@ public class IotModbusTcpSlaveUpstreamHandler { | @@ -105,29 +104,14 @@ public class IotModbusTcpSlaveUpstreamHandler { | ||
| 105 | return; | 104 | return; |
| 106 | } | 105 | } |
| 107 | 106 | ||
| 108 | - // 2. 自定义功能码(认证等扩展) | 107 | + // 2. 情况一:自定义功能码(认证等扩展) |
| 109 | if (StrUtil.isNotEmpty(frame.getCustomData())) { | 108 | if (StrUtil.isNotEmpty(frame.getCustomData())) { |
| 110 | handleCustomFrame(socket, frame, frameFormat); | 109 | handleCustomFrame(socket, frame, frameFormat); |
| 111 | return; | 110 | return; |
| 112 | } | 111 | } |
| 113 | 112 | ||
| 114 | - // 1.2 未认证连接,丢弃 | ||
| 115 | - // TODO @AI:把 1.2、1.3 拿到 handlePollingResponse 里;是否需要登录,自己知道! | ||
| 116 | - if (!connectionManager.isAuthenticated(socket)) { | ||
| 117 | - log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); | ||
| 118 | - return; | ||
| 119 | - } | ||
| 120 | - | ||
| 121 | - // 3. DONE @AI:断言必须是云端轮询(不再支持主动上报) | ||
| 122 | - // TODO @AI:貌似只能轮询到一次?! | ||
| 123 | - // 1.3 标准 Modbus 响应(只支持云端轮询模式) | ||
| 124 | - // TODO @AI:可以把 | ||
| 125 | - ConnectionInfo info = connectionManager.getConnectionInfo(socket); | ||
| 126 | - if (info == null) { | ||
| 127 | - log.warn("[handleFrame][已认证但连接信息为空, remoteAddress={}]", socket.remoteAddress()); | ||
| 128 | - return; | ||
| 129 | - } | ||
| 130 | - handlePollingResponse(info, frame, frameFormat); | 113 | + // 3. 情况二:标准 Modbus 响应 → 轮询响应处理 |
| 114 | + handlePollingResponse(socket, frame, frameFormat); | ||
| 131 | } | 115 | } |
| 132 | 116 | ||
| 133 | // ========== 自定义 FC 处理(认证等) ========== | 117 | // ========== 自定义 FC 处理(认证等) ========== |
| @@ -138,96 +122,83 @@ public class IotModbusTcpSlaveUpstreamHandler { | @@ -138,96 +122,83 @@ public class IotModbusTcpSlaveUpstreamHandler { | ||
| 138 | * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler} | 122 | * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler} |
| 139 | */ | 123 | */ |
| 140 | private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { | 124 | private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { |
| 125 | + String method = null; | ||
| 141 | try { | 126 | try { |
| 142 | - JSONObject json = JSONUtil.parseObj(frame.getCustomData()); | ||
| 143 | - String method = json.getStr("method"); | 127 | + IotDeviceMessage message = JsonUtils.parseObject(frame.getCustomData(), IotDeviceMessage.class); |
| 128 | + if (message == null) { | ||
| 129 | + throw invalidParamException("自定义 FC 数据解析失败"); | ||
| 130 | + } | ||
| 131 | + method = message.getMethod(); | ||
| 144 | if (METHOD_AUTH.equals(method)) { | 132 | if (METHOD_AUTH.equals(method)) { |
| 145 | - handleAuth(socket, frame, json, frameFormat); | 133 | + handleAuth(socket, frame, frameFormat, message.getParams()); |
| 146 | return; | 134 | return; |
| 147 | } | 135 | } |
| 148 | log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]", | 136 | log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]", |
| 149 | method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData()); | 137 | method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData()); |
| 150 | } catch (ServiceException e) { | 138 | } catch (ServiceException e) { |
| 151 | // 已知业务异常,返回对应的错误码和错误信息 | 139 | // 已知业务异常,返回对应的错误码和错误信息 |
| 152 | - sendCustomResponse(socket, frame, frameFormat, e.getCode(), e.getMessage()); | 140 | + sendCustomResponse(socket, frame, frameFormat, method, e.getCode(), e.getMessage()); |
| 153 | } catch (IllegalArgumentException e) { | 141 | } catch (IllegalArgumentException e) { |
| 154 | // 参数校验异常,返回 400 错误 | 142 | // 参数校验异常,返回 400 错误 |
| 155 | - sendCustomResponse(socket, frame, frameFormat, BAD_REQUEST.getCode(), e.getMessage()); | 143 | + sendCustomResponse(socket, frame, frameFormat, method, BAD_REQUEST.getCode(), e.getMessage()); |
| 156 | } catch (Exception e) { | 144 | } catch (Exception e) { |
| 157 | // 其他未知异常,返回 500 错误 | 145 | // 其他未知异常,返回 500 错误 |
| 158 | log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]", | 146 | log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]", |
| 159 | frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e); | 147 | frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e); |
| 160 | - sendCustomResponse(socket, frame, frameFormat, | 148 | + sendCustomResponse(socket, frame, frameFormat, method, |
| 161 | INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | 149 | INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); |
| 162 | } | 150 | } |
| 163 | } | 151 | } |
| 164 | 152 | ||
| 165 | - // TODO @芋艿:在 review 下这个类; | ||
| 166 | - // TODO @AI:不传递 json,直接在 frame | ||
| 167 | /** | 153 | /** |
| 168 | * 处理认证请求 | 154 | * 处理认证请求 |
| 169 | */ | 155 | */ |
| 170 | - private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json, | ||
| 171 | - IotModbusFrameFormatEnum frameFormat) { | ||
| 172 | - // TODO @AI:是不是可以 JsonUtils.convert(json, IotDeviceAuthReqDTO.class); | ||
| 173 | - JSONObject params = json.getJSONObject("params"); | ||
| 174 | - if (params == null) { | ||
| 175 | - throw invalidParamException("params 不能为空"); | ||
| 176 | - } | ||
| 177 | - // DONE @AI:参数判空 | ||
| 178 | - String clientId = params.getStr("clientId"); | ||
| 179 | - String username = params.getStr("username"); | ||
| 180 | - String password = params.getStr("password"); | ||
| 181 | - // TODO @AI:逐个判空; | ||
| 182 | - if (StrUtil.hasBlank(clientId, username, password)) { | ||
| 183 | - throw invalidParamException("clientId、username、password 不能为空"); | ||
| 184 | - } | 156 | + @SuppressWarnings("DataFlowIssue") |
| 157 | + private void handleAuth(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat, Object params) { | ||
| 158 | + // 1. 解析认证参数 | ||
| 159 | + IotDeviceAuthReqDTO request = JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class); | ||
| 160 | + Assert.notNull(request, "认证参数不能为空"); | ||
| 161 | + Assert.notBlank(request.getClientId(), "clientId 不能为空"); | ||
| 162 | + Assert.notBlank(request.getUsername(), "username 不能为空"); | ||
| 163 | + Assert.notBlank(request.getPassword(), "password 不能为空"); | ||
| 185 | 164 | ||
| 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, "认证失败"); | 165 | + // 2.1 调用认证 API |
| 166 | + CommonResult<Boolean> result = deviceApi.authDevice(request); | ||
| 167 | + result.checkError(); | ||
| 168 | + if (BooleanUtil.isFalse(result.getData())) { | ||
| 169 | + log.warn("[handleAuth][认证失败, clientId={}, username={}]", request.getClientId(), request.getUsername()); | ||
| 170 | + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, 1, "认证失败"); | ||
| 194 | return; | 171 | return; |
| 195 | } | 172 | } |
| 196 | - | ||
| 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 解析设备信息 | 173 | // 2.2 解析设备信息 |
| 203 | - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); | 174 | + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(request.getUsername()); |
| 204 | Assert.notNull(deviceInfo, "解析设备信息失败"); | 175 | Assert.notNull(deviceInfo, "解析设备信息失败"); |
| 205 | // 2.3 获取设备信息 | 176 | // 2.3 获取设备信息 |
| 206 | - // DONE @AI:IotDeviceService 作为构造参数传入,不通过 SpringUtil.getBean | ||
| 207 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | 177 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); |
| 208 | Assert.notNull(device, "设备不存在"); | 178 | Assert.notNull(device, "设备不存在"); |
| 209 | 179 | ||
| 210 | - // 3. 注册连接 | 180 | + // 3.1 注册连接 |
| 211 | ConnectionInfo connectionInfo = new ConnectionInfo() | 181 | ConnectionInfo connectionInfo = new ConnectionInfo() |
| 212 | .setDeviceId(device.getId()) | 182 | .setDeviceId(device.getId()) |
| 183 | + .setProductKey(deviceInfo.getProductKey()) | ||
| 184 | + .setDeviceName(deviceInfo.getDeviceName()) | ||
| 213 | .setSlaveId(frame.getSlaveId()) | 185 | .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 | - } | 186 | + .setFrameFormat(frameFormat); |
| 221 | connectionManager.registerConnection(socket, connectionInfo); | 187 | connectionManager.registerConnection(socket, connectionInfo); |
| 188 | + // 3.2 发送上线消息 | ||
| 189 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | ||
| 190 | + messageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); | ||
| 191 | + // 3.3 发送成功响应 | ||
| 192 | + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, | ||
| 193 | + GlobalErrorCodeConstants.SUCCESS.getCode(), "success"); | ||
| 194 | + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", request.getClientId(), device.getId()); | ||
| 222 | 195 | ||
| 223 | - // 4. 发送认证成功响应 | ||
| 224 | - sendCustomResponse(socket, frame, frameFormat, 0, "success"); | ||
| 225 | - log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId, | ||
| 226 | - config != null ? config.getDeviceId() : device.getId()); | ||
| 227 | - | ||
| 228 | - // 5. 直接启动轮询 | 196 | + // 4. 加载设备配置并启动轮询 |
| 197 | + IotModbusDeviceConfigRespDTO config = configCacheService.loadDeviceConfig(device.getId()); | ||
| 229 | if (config != null) { | 198 | if (config != null) { |
| 230 | pollScheduler.updatePolling(config); | 199 | pollScheduler.updatePolling(config); |
| 200 | + } else { | ||
| 201 | + log.warn("[handleAuth][认证成功但未找到设备配置, deviceId={}]", device.getId()); | ||
| 231 | } | 202 | } |
| 232 | } | 203 | } |
| 233 | 204 | ||
| @@ -236,12 +207,13 @@ public class IotModbusTcpSlaveUpstreamHandler { | @@ -236,12 +207,13 @@ public class IotModbusTcpSlaveUpstreamHandler { | ||
| 236 | */ | 207 | */ |
| 237 | private void sendCustomResponse(NetSocket socket, IotModbusFrame frame, | 208 | private void sendCustomResponse(NetSocket socket, IotModbusFrame frame, |
| 238 | IotModbusFrameFormatEnum frameFormat, | 209 | IotModbusFrameFormatEnum frameFormat, |
| 239 | - int code, String message) { | ||
| 240 | - JSONObject resp = new JSONObject(); | ||
| 241 | - resp.set("method", METHOD_AUTH); | ||
| 242 | - resp.set("code", code); | ||
| 243 | - resp.set("message", message); | ||
| 244 | - byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), resp.toString(), | 210 | + String method, int code, String message) { |
| 211 | + Map<String, Object> response = MapUtil.<String, Object>builder() | ||
| 212 | + .put("method", method) | ||
| 213 | + .put("code", code) | ||
| 214 | + .put("message", message) | ||
| 215 | + .build(); | ||
| 216 | + byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response), | ||
| 245 | frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); | 217 | frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); |
| 246 | connectionManager.sendToSocket(socket, data); | 218 | connectionManager.sendToSocket(socket, data); |
| 247 | } | 219 | } |
| @@ -251,9 +223,16 @@ public class IotModbusTcpSlaveUpstreamHandler { | @@ -251,9 +223,16 @@ public class IotModbusTcpSlaveUpstreamHandler { | ||
| 251 | /** | 223 | /** |
| 252 | * 处理轮询响应(云端轮询模式) | 224 | * 处理轮询响应(云端轮询模式) |
| 253 | */ | 225 | */ |
| 254 | - private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame, | 226 | + private void handlePollingResponse(NetSocket socket, IotModbusFrame frame, |
| 255 | IotModbusFrameFormatEnum frameFormat) { | 227 | IotModbusFrameFormatEnum frameFormat) { |
| 256 | - // 1.1 匹配 PendingRequest | 228 | + // 1. 获取连接信息(未认证连接丢弃) |
| 229 | + ConnectionInfo info = connectionManager.getConnectionInfo(socket); | ||
| 230 | + if (info == null) { | ||
| 231 | + log.warn("[handlePollingResponse][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress()); | ||
| 232 | + return; | ||
| 233 | + } | ||
| 234 | + | ||
| 235 | + // 2.1 匹配 PendingRequest | ||
| 257 | PendingRequest request = pendingRequestManager.matchResponse( | 236 | PendingRequest request = pendingRequestManager.matchResponse( |
| 258 | info.getDeviceId(), frame, frameFormat); | 237 | info.getDeviceId(), frame, frameFormat); |
| 259 | if (request == null) { | 238 | if (request == null) { |
| @@ -261,26 +240,27 @@ public class IotModbusTcpSlaveUpstreamHandler { | @@ -261,26 +240,27 @@ public class IotModbusTcpSlaveUpstreamHandler { | ||
| 261 | info.getDeviceId(), frame.getFunctionCode()); | 240 | info.getDeviceId(), frame.getFunctionCode()); |
| 262 | return; | 241 | return; |
| 263 | } | 242 | } |
| 264 | - // 1.2 提取寄存器值 | 243 | + // 2.2 提取寄存器值 |
| 265 | int[] rawValues = IotModbusUtils.extractValues(frame); | 244 | int[] rawValues = IotModbusUtils.extractValues(frame); |
| 266 | if (rawValues == null) { | 245 | if (rawValues == null) { |
| 267 | log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", | 246 | log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", |
| 268 | info.getDeviceId(), request.getIdentifier()); | 247 | info.getDeviceId(), request.getIdentifier()); |
| 269 | return; | 248 | return; |
| 270 | } | 249 | } |
| 271 | - // 1.3 查找点位配置 | 250 | + // 2.3 查找点位配置 |
| 272 | IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); | 251 | IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); |
| 273 | if (config == null || CollUtil.isEmpty(config.getPoints())) { | 252 | if (config == null || CollUtil.isEmpty(config.getPoints())) { |
| 274 | return; | 253 | return; |
| 275 | } | 254 | } |
| 276 | - var point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(request.getPointId())); | 255 | + IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), |
| 256 | + p -> p.getId().equals(request.getPointId())); | ||
| 277 | if (point == null) { | 257 | if (point == null) { |
| 278 | return; | 258 | return; |
| 279 | } | 259 | } |
| 280 | 260 | ||
| 281 | - // 2.1 点位翻译 | 261 | + // 3.1 点位翻译 |
| 282 | Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); | 262 | Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); |
| 283 | - // 2.2 上报属性 | 263 | + // 3.2 上报属性 |
| 284 | Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue); | 264 | Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue); |
| 285 | IotDeviceMessage message = IotDeviceMessage.requestOf( | 265 | IotDeviceMessage message = IotDeviceMessage.requestOf( |
| 286 | IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); | 266 | IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java
| @@ -80,7 +80,7 @@ public class IotModbusTcpSlaveConfigCacheService { | @@ -80,7 +80,7 @@ public class IotModbusTcpSlaveConfigCacheService { | ||
| 80 | */ | 80 | */ |
| 81 | private List<IotModbusDeviceConfigRespDTO> buildMockConfigs() { | 81 | private List<IotModbusDeviceConfigRespDTO> buildMockConfigs() { |
| 82 | IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO(); | 82 | IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO(); |
| 83 | - config.setDeviceId(1L); | 83 | + config.setDeviceId(25L); |
| 84 | config.setProductKey("4aymZgOTOOCrDKRT"); | 84 | config.setProductKey("4aymZgOTOOCrDKRT"); |
| 85 | config.setDeviceName("small"); | 85 | config.setDeviceName("small"); |
| 86 | config.setSlaveId(1); | 86 | config.setSlaveId(1); |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java
| @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; | 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; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; |
| 11 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils; | 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; |
| 12 | import io.vertx.core.Vertx; | 12 | import io.vertx.core.Vertx; |
| 13 | import io.vertx.core.buffer.Buffer; | 13 | import io.vertx.core.buffer.Buffer; |
| 14 | import io.vertx.core.net.NetClient; | 14 | import io.vertx.core.net.NetClient; |