Commit e13cd545cced2a8f7ae11e3f80aeca5c428fcc08
1 parent
4d578b23
feat(iot):modbus-tcp-slave 继续优化,处理各种连接的边界、轮询的间隔等
Showing
19 changed files
with
814 additions
and
459 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusDataConverter.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common; | |
| 2 | - | |
| 3 | -import cn.hutool.core.util.ArrayUtil; | |
| 4 | -import cn.hutool.core.util.ObjectUtil; | |
| 5 | -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; | |
| 6 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; | |
| 7 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; | |
| 8 | -import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; | |
| 9 | -import lombok.extern.slf4j.Slf4j; | |
| 10 | - | |
| 11 | -import java.math.BigDecimal; | |
| 12 | -import java.math.RoundingMode; | |
| 13 | -import java.nio.ByteBuffer; | |
| 14 | -import java.nio.ByteOrder; | |
| 15 | - | |
| 16 | -/** | |
| 17 | - * IoT Modbus 数据转换器 | |
| 18 | - * <p> | |
| 19 | - * 负责 Modbus 原始寄存器值与物模型属性值的相互转换: | |
| 20 | - * 1. 将 Modbus 原始寄存器值转换为物模型属性值 | |
| 21 | - * 2. 将物模型属性值转换为 Modbus 原始寄存器值 | |
| 22 | - * | |
| 23 | - * @author 芋道源码 | |
| 24 | - */ | |
| 25 | -@Slf4j | |
| 26 | -public class IotModbusDataConverter { | |
| 27 | - | |
| 28 | - /** | |
| 29 | - * 将原始值转换为物模型属性值 | |
| 30 | - * | |
| 31 | - * @param rawValues 原始值数组(寄存器值或线圈值) | |
| 32 | - * @param point 点位配置 | |
| 33 | - * @return 转换后的属性值 | |
| 34 | - */ | |
| 35 | - public Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { | |
| 36 | - if (ArrayUtil.isEmpty(rawValues)) { | |
| 37 | - return null; | |
| 38 | - } | |
| 39 | - String rawDataType = point.getRawDataType(); | |
| 40 | - String byteOrder = point.getByteOrder(); | |
| 41 | - BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); | |
| 42 | - | |
| 43 | - // 1. 根据原始数据类型解析原始数值 | |
| 44 | - Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); | |
| 45 | - if (rawNumber == null) { | |
| 46 | - return null; | |
| 47 | - } | |
| 48 | - | |
| 49 | - // 2. 应用缩放因子:实际值 = 原始值 × scale | |
| 50 | - BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); | |
| 51 | - | |
| 52 | - // 3. 根据数据类型返回合适的 Java 类型 | |
| 53 | - return formatValue(actualValue, rawDataType); | |
| 54 | - } | |
| 55 | - | |
| 56 | - /** | |
| 57 | - * 将物模型属性值转换为原始寄存器值 | |
| 58 | - * | |
| 59 | - * @param propertyValue 属性值 | |
| 60 | - * @param point 点位配置 | |
| 61 | - * @return 原始值数组 | |
| 62 | - */ | |
| 63 | - public int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { | |
| 64 | - if (propertyValue == null) { | |
| 65 | - return new int[0]; | |
| 66 | - } | |
| 67 | - String rawDataType = point.getRawDataType(); | |
| 68 | - String byteOrder = point.getByteOrder(); | |
| 69 | - BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); | |
| 70 | - int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1); | |
| 71 | - | |
| 72 | - // 1. 转换为 BigDecimal | |
| 73 | - BigDecimal actualValue = new BigDecimal(propertyValue.toString()); | |
| 74 | - | |
| 75 | - // 2. 应用缩放因子:原始值 = 实际值 ÷ scale | |
| 76 | - BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); | |
| 77 | - | |
| 78 | - // 3. 根据原始数据类型编码为寄存器值 | |
| 79 | - return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); | |
| 80 | - } | |
| 81 | - | |
| 82 | - /** | |
| 83 | - * 解析原始值 | |
| 84 | - */ | |
| 85 | - @SuppressWarnings("EnhancedSwitchMigration") | |
| 86 | - private Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { | |
| 87 | - IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); | |
| 88 | - if (dataTypeEnum == null) { | |
| 89 | - log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); | |
| 90 | - return rawValues[0]; | |
| 91 | - } | |
| 92 | - switch (dataTypeEnum) { | |
| 93 | - case BOOLEAN: | |
| 94 | - return rawValues[0] != 0 ? 1 : 0; | |
| 95 | - case INT16: | |
| 96 | - return (short) rawValues[0]; | |
| 97 | - case UINT16: | |
| 98 | - return rawValues[0] & 0xFFFF; | |
| 99 | - case INT32: | |
| 100 | - return parseInt32(rawValues, byteOrder); | |
| 101 | - case UINT32: | |
| 102 | - return parseUint32(rawValues, byteOrder); | |
| 103 | - case FLOAT: | |
| 104 | - return parseFloat(rawValues, byteOrder); | |
| 105 | - case DOUBLE: | |
| 106 | - return parseDouble(rawValues, byteOrder); | |
| 107 | - default: | |
| 108 | - log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); | |
| 109 | - return rawValues[0]; | |
| 110 | - } | |
| 111 | - } | |
| 112 | - | |
| 113 | - private int parseInt32(int[] rawValues, String byteOrder) { | |
| 114 | - if (rawValues.length < 2) { | |
| 115 | - return rawValues[0]; | |
| 116 | - } | |
| 117 | - byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | |
| 118 | - return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); | |
| 119 | - } | |
| 120 | - | |
| 121 | - private long parseUint32(int[] rawValues, String byteOrder) { | |
| 122 | - if (rawValues.length < 2) { | |
| 123 | - return rawValues[0] & 0xFFFFFFFFL; | |
| 124 | - } | |
| 125 | - byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | |
| 126 | - return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; | |
| 127 | - } | |
| 128 | - | |
| 129 | - private float parseFloat(int[] rawValues, String byteOrder) { | |
| 130 | - if (rawValues.length < 2) { | |
| 131 | - return (float) rawValues[0]; | |
| 132 | - } | |
| 133 | - byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | |
| 134 | - return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); | |
| 135 | - } | |
| 136 | - | |
| 137 | - private double parseDouble(int[] rawValues, String byteOrder) { | |
| 138 | - if (rawValues.length < 4) { | |
| 139 | - return rawValues[0]; | |
| 140 | - } | |
| 141 | - byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); | |
| 142 | - return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); | |
| 143 | - } | |
| 144 | - | |
| 145 | - /** | |
| 146 | - * 将寄存器值转换为字节数组 | |
| 147 | - */ | |
| 148 | - private byte[] registersToBytes(int[] registers, int count) { | |
| 149 | - byte[] bytes = new byte[count * 2]; | |
| 150 | - for (int i = 0; i < Math.min(registers.length, count); i++) { | |
| 151 | - bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); | |
| 152 | - bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); | |
| 153 | - } | |
| 154 | - return bytes; | |
| 155 | - } | |
| 156 | - | |
| 157 | - /** | |
| 158 | - * 根据字节序重排字节 | |
| 159 | - */ | |
| 160 | - @SuppressWarnings("EnhancedSwitchMigration") | |
| 161 | - private byte[] reorderBytes(byte[] bytes, String byteOrder) { | |
| 162 | - IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder); | |
| 163 | - // null 或者大端序,不需要调整 | |
| 164 | - if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) { | |
| 165 | - return bytes; | |
| 166 | - } | |
| 167 | - | |
| 168 | - // 其他字节序调整 | |
| 169 | - byte[] result = new byte[bytes.length]; | |
| 170 | - switch (byteOrderEnum) { | |
| 171 | - case BA: // 小端序(16 位) | |
| 172 | - if (bytes.length >= 2) { | |
| 173 | - result[0] = bytes[1]; | |
| 174 | - result[1] = bytes[0]; | |
| 175 | - } | |
| 176 | - break; | |
| 177 | - case CDAB: // 大端字交换(32 位) | |
| 178 | - if (bytes.length >= 4) { | |
| 179 | - result[0] = bytes[2]; | |
| 180 | - result[1] = bytes[3]; | |
| 181 | - result[2] = bytes[0]; | |
| 182 | - result[3] = bytes[1]; | |
| 183 | - } | |
| 184 | - break; | |
| 185 | - case DCBA: // 小端序(32 位) | |
| 186 | - if (bytes.length >= 4) { | |
| 187 | - result[0] = bytes[3]; | |
| 188 | - result[1] = bytes[2]; | |
| 189 | - result[2] = bytes[1]; | |
| 190 | - result[3] = bytes[0]; | |
| 191 | - } | |
| 192 | - break; | |
| 193 | - case BADC: // 小端字交换(32 位) | |
| 194 | - if (bytes.length >= 4) { | |
| 195 | - result[0] = bytes[1]; | |
| 196 | - result[1] = bytes[0]; | |
| 197 | - result[2] = bytes[3]; | |
| 198 | - result[3] = bytes[2]; | |
| 199 | - } | |
| 200 | - break; | |
| 201 | - default: | |
| 202 | - return bytes; | |
| 203 | - } | |
| 204 | - return result; | |
| 205 | - } | |
| 206 | - | |
| 207 | - /** | |
| 208 | - * 编码为寄存器值 | |
| 209 | - */ | |
| 210 | - @SuppressWarnings("EnhancedSwitchMigration") | |
| 211 | - private int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { | |
| 212 | - IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); | |
| 213 | - if (dataTypeEnum == null) { | |
| 214 | - return new int[]{rawValue.intValue()}; | |
| 215 | - } | |
| 216 | - switch (dataTypeEnum) { | |
| 217 | - case BOOLEAN: | |
| 218 | - return new int[]{rawValue.intValue() != 0 ? 1 : 0}; | |
| 219 | - case INT16: | |
| 220 | - case UINT16: | |
| 221 | - return new int[]{rawValue.intValue() & 0xFFFF}; | |
| 222 | - case INT32: | |
| 223 | - case UINT32: | |
| 224 | - return encodeInt32(rawValue.intValue(), byteOrder); | |
| 225 | - case FLOAT: | |
| 226 | - return encodeFloat(rawValue.floatValue(), byteOrder); | |
| 227 | - case DOUBLE: | |
| 228 | - return encodeDouble(rawValue.doubleValue(), byteOrder); | |
| 229 | - default: | |
| 230 | - return new int[]{rawValue.intValue()}; | |
| 231 | - } | |
| 232 | - } | |
| 233 | - | |
| 234 | - private int[] encodeInt32(int value, String byteOrder) { | |
| 235 | - byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); | |
| 236 | - bytes = reorderBytes(bytes, byteOrder); | |
| 237 | - return bytesToRegisters(bytes); | |
| 238 | - } | |
| 239 | - | |
| 240 | - private int[] encodeFloat(float value, String byteOrder) { | |
| 241 | - byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); | |
| 242 | - bytes = reorderBytes(bytes, byteOrder); | |
| 243 | - return bytesToRegisters(bytes); | |
| 244 | - } | |
| 245 | - | |
| 246 | - private int[] encodeDouble(double value, String byteOrder) { | |
| 247 | - byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); | |
| 248 | - bytes = reorderBytes(bytes, byteOrder); | |
| 249 | - return bytesToRegisters(bytes); | |
| 250 | - } | |
| 251 | - | |
| 252 | - private int[] bytesToRegisters(byte[] bytes) { | |
| 253 | - int[] registers = new int[bytes.length / 2]; | |
| 254 | - for (int i = 0; i < registers.length; i++) { | |
| 255 | - registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); | |
| 256 | - } | |
| 257 | - return registers; | |
| 258 | - } | |
| 259 | - | |
| 260 | - /** | |
| 261 | - * 格式化返回值 | |
| 262 | - */ | |
| 263 | - @SuppressWarnings("EnhancedSwitchMigration") | |
| 264 | - private Object formatValue(BigDecimal value, String rawDataType) { | |
| 265 | - IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); | |
| 266 | - if (dataTypeEnum == null) { | |
| 267 | - return value; | |
| 268 | - } | |
| 269 | - switch (dataTypeEnum) { | |
| 270 | - case BOOLEAN: | |
| 271 | - return value.intValue() != 0; | |
| 272 | - case INT16: | |
| 273 | - case INT32: | |
| 274 | - return value.intValue(); | |
| 275 | - case UINT16: | |
| 276 | - case UINT32: | |
| 277 | - return value.longValue(); | |
| 278 | - case FLOAT: | |
| 279 | - return value.floatValue(); | |
| 280 | - case DOUBLE: | |
| 281 | - return value.doubleValue(); | |
| 282 | - default: | |
| 283 | - return value; | |
| 284 | - } | |
| 285 | - } | |
| 286 | - | |
| 287 | -} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.hutool.core.util.ArrayUtil; | |
| 5 | +import cn.hutool.core.util.ObjectUtil; | |
| 6 | +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; | |
| 4 | 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 5 | 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 9 | +import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; | |
| 10 | +import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; | |
| 6 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; |
| 12 | +import lombok.experimental.UtilityClass; | |
| 7 | 13 | import lombok.extern.slf4j.Slf4j; |
| 8 | 14 | |
| 15 | +import java.math.BigDecimal; | |
| 16 | +import java.math.RoundingMode; | |
| 17 | +import java.nio.ByteBuffer; | |
| 18 | +import java.nio.ByteOrder; | |
| 19 | + | |
| 9 | 20 | /** |
| 10 | - * IoT Modbus 工具类 | |
| 21 | + * IoT Modbus 协议工具类 | |
| 11 | 22 | * <p> |
| 12 | - * 提供: | |
| 13 | - * 1. Modbus 协议常量(功能码、掩码等) | |
| 14 | - * 2. CRC-16/MODBUS 计算和校验 | |
| 15 | - * 3. 功能码分类判断 | |
| 16 | - * 4. 从解码后的 ${IotModbusFrame} 中提取寄存器值(用于后续的点位翻译) | |
| 23 | + * 提供 Modbus 协议全链路能力: | |
| 24 | + * <ul> | |
| 25 | + * <li>协议常量:功能码(FC01~FC16)、异常掩码等</li> | |
| 26 | + * <li>功能码判断:读/写/异常分类、可写判断、写功能码映射</li> | |
| 27 | + * <li>CRC-16/MODBUS 计算和校验</li> | |
| 28 | + * <li>数据转换:原始值 ↔ 物模型属性值({@link #convertToPropertyValue} / {@link #convertToRawValues})</li> | |
| 29 | + * <li>帧值提取:从 Modbus 帧提取寄存器/线圈值({@link #extractValues})</li> | |
| 30 | + * <li>点位查找({@link #findPoint})</li> | |
| 31 | + * </ul> | |
| 17 | 32 | * |
| 18 | 33 | * @author 芋道源码 |
| 19 | 34 | */ |
| 35 | +@UtilityClass | |
| 20 | 36 | @Slf4j |
| 21 | 37 | public class IotModbusUtils { |
| 22 | 38 | |
| ... | ... | @@ -139,19 +155,6 @@ public class IotModbusUtils { |
| 139 | 155 | } |
| 140 | 156 | } |
| 141 | 157 | |
| 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 | 158 | // ==================== CRC16 工具 ==================== |
| 156 | 159 | |
| 157 | 160 | /** |
| ... | ... | @@ -192,7 +195,253 @@ public class IotModbusUtils { |
| 192 | 195 | return computed == received; |
| 193 | 196 | } |
| 194 | 197 | |
| 195 | - // ==================== 响应值提取 ==================== | |
| 198 | + // ==================== 数据转换 ==================== | |
| 199 | + | |
| 200 | + /** | |
| 201 | + * 将原始值转换为物模型属性值 | |
| 202 | + * | |
| 203 | + * @param rawValues 原始值数组(寄存器值或线圈值) | |
| 204 | + * @param point 点位配置 | |
| 205 | + * @return 转换后的属性值 | |
| 206 | + */ | |
| 207 | + public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { | |
| 208 | + if (ArrayUtil.isEmpty(rawValues)) { | |
| 209 | + return null; | |
| 210 | + } | |
| 211 | + String rawDataType = point.getRawDataType(); | |
| 212 | + String byteOrder = point.getByteOrder(); | |
| 213 | + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); | |
| 214 | + | |
| 215 | + // 1. 根据原始数据类型解析原始数值 | |
| 216 | + Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); | |
| 217 | + if (rawNumber == null) { | |
| 218 | + return null; | |
| 219 | + } | |
| 220 | + | |
| 221 | + // 2. 应用缩放因子:实际值 = 原始值 × scale | |
| 222 | + BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); | |
| 223 | + | |
| 224 | + // 3. 根据数据类型返回合适的 Java 类型 | |
| 225 | + return formatValue(actualValue, rawDataType); | |
| 226 | + } | |
| 227 | + | |
| 228 | + /** | |
| 229 | + * 将物模型属性值转换为原始寄存器值 | |
| 230 | + * | |
| 231 | + * @param propertyValue 属性值 | |
| 232 | + * @param point 点位配置 | |
| 233 | + * @return 原始值数组 | |
| 234 | + */ | |
| 235 | + public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { | |
| 236 | + if (propertyValue == null) { | |
| 237 | + return new int[0]; | |
| 238 | + } | |
| 239 | + String rawDataType = point.getRawDataType(); | |
| 240 | + String byteOrder = point.getByteOrder(); | |
| 241 | + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); | |
| 242 | + int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1); | |
| 243 | + | |
| 244 | + // 1. 转换为 BigDecimal | |
| 245 | + BigDecimal actualValue = new BigDecimal(propertyValue.toString()); | |
| 246 | + | |
| 247 | + // 2. 应用缩放因子:原始值 = 实际值 ÷ scale | |
| 248 | + BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); | |
| 249 | + | |
| 250 | + // 3. 根据原始数据类型编码为寄存器值 | |
| 251 | + return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); | |
| 252 | + } | |
| 253 | + | |
| 254 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 255 | + private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { | |
| 256 | + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); | |
| 257 | + if (dataTypeEnum == null) { | |
| 258 | + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); | |
| 259 | + return rawValues[0]; | |
| 260 | + } | |
| 261 | + switch (dataTypeEnum) { | |
| 262 | + case BOOLEAN: | |
| 263 | + return rawValues[0] != 0 ? 1 : 0; | |
| 264 | + case INT16: | |
| 265 | + return (short) rawValues[0]; | |
| 266 | + case UINT16: | |
| 267 | + return rawValues[0] & 0xFFFF; | |
| 268 | + case INT32: | |
| 269 | + return parseInt32(rawValues, byteOrder); | |
| 270 | + case UINT32: | |
| 271 | + return parseUint32(rawValues, byteOrder); | |
| 272 | + case FLOAT: | |
| 273 | + return parseFloat(rawValues, byteOrder); | |
| 274 | + case DOUBLE: | |
| 275 | + return parseDouble(rawValues, byteOrder); | |
| 276 | + default: | |
| 277 | + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); | |
| 278 | + return rawValues[0]; | |
| 279 | + } | |
| 280 | + } | |
| 281 | + | |
| 282 | + private static int parseInt32(int[] rawValues, String byteOrder) { | |
| 283 | + if (rawValues.length < 2) { | |
| 284 | + return rawValues[0]; | |
| 285 | + } | |
| 286 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | |
| 287 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); | |
| 288 | + } | |
| 289 | + | |
| 290 | + private static long parseUint32(int[] rawValues, String byteOrder) { | |
| 291 | + if (rawValues.length < 2) { | |
| 292 | + return rawValues[0] & 0xFFFFFFFFL; | |
| 293 | + } | |
| 294 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | |
| 295 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; | |
| 296 | + } | |
| 297 | + | |
| 298 | + private static float parseFloat(int[] rawValues, String byteOrder) { | |
| 299 | + if (rawValues.length < 2) { | |
| 300 | + return (float) rawValues[0]; | |
| 301 | + } | |
| 302 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); | |
| 303 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); | |
| 304 | + } | |
| 305 | + | |
| 306 | + private static double parseDouble(int[] rawValues, String byteOrder) { | |
| 307 | + if (rawValues.length < 4) { | |
| 308 | + return rawValues[0]; | |
| 309 | + } | |
| 310 | + byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); | |
| 311 | + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); | |
| 312 | + } | |
| 313 | + | |
| 314 | + private static byte[] registersToBytes(int[] registers, int count) { | |
| 315 | + byte[] bytes = new byte[count * 2]; | |
| 316 | + for (int i = 0; i < Math.min(registers.length, count); i++) { | |
| 317 | + bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); | |
| 318 | + bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); | |
| 319 | + } | |
| 320 | + return bytes; | |
| 321 | + } | |
| 322 | + | |
| 323 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 324 | + private static byte[] reorderBytes(byte[] bytes, String byteOrder) { | |
| 325 | + IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder); | |
| 326 | + // null 或者大端序,不需要调整 | |
| 327 | + if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) { | |
| 328 | + return bytes; | |
| 329 | + } | |
| 330 | + | |
| 331 | + // 其他字节序调整 | |
| 332 | + byte[] result = new byte[bytes.length]; | |
| 333 | + switch (byteOrderEnum) { | |
| 334 | + case BA: // 小端序(16 位) | |
| 335 | + if (bytes.length >= 2) { | |
| 336 | + result[0] = bytes[1]; | |
| 337 | + result[1] = bytes[0]; | |
| 338 | + } | |
| 339 | + break; | |
| 340 | + case CDAB: // 大端字交换(32 位) | |
| 341 | + if (bytes.length >= 4) { | |
| 342 | + result[0] = bytes[2]; | |
| 343 | + result[1] = bytes[3]; | |
| 344 | + result[2] = bytes[0]; | |
| 345 | + result[3] = bytes[1]; | |
| 346 | + } | |
| 347 | + break; | |
| 348 | + case DCBA: // 小端序(32 位) | |
| 349 | + if (bytes.length >= 4) { | |
| 350 | + result[0] = bytes[3]; | |
| 351 | + result[1] = bytes[2]; | |
| 352 | + result[2] = bytes[1]; | |
| 353 | + result[3] = bytes[0]; | |
| 354 | + } | |
| 355 | + break; | |
| 356 | + case BADC: // 小端字交换(32 位) | |
| 357 | + if (bytes.length >= 4) { | |
| 358 | + result[0] = bytes[1]; | |
| 359 | + result[1] = bytes[0]; | |
| 360 | + result[2] = bytes[3]; | |
| 361 | + result[3] = bytes[2]; | |
| 362 | + } | |
| 363 | + break; | |
| 364 | + default: | |
| 365 | + return bytes; | |
| 366 | + } | |
| 367 | + return result; | |
| 368 | + } | |
| 369 | + | |
| 370 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 371 | + private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { | |
| 372 | + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); | |
| 373 | + if (dataTypeEnum == null) { | |
| 374 | + return new int[]{rawValue.intValue()}; | |
| 375 | + } | |
| 376 | + switch (dataTypeEnum) { | |
| 377 | + case BOOLEAN: | |
| 378 | + return new int[]{rawValue.intValue() != 0 ? 1 : 0}; | |
| 379 | + case INT16: | |
| 380 | + case UINT16: | |
| 381 | + return new int[]{rawValue.intValue() & 0xFFFF}; | |
| 382 | + case INT32: | |
| 383 | + case UINT32: | |
| 384 | + return encodeInt32(rawValue.intValue(), byteOrder); | |
| 385 | + case FLOAT: | |
| 386 | + return encodeFloat(rawValue.floatValue(), byteOrder); | |
| 387 | + case DOUBLE: | |
| 388 | + return encodeDouble(rawValue.doubleValue(), byteOrder); | |
| 389 | + default: | |
| 390 | + return new int[]{rawValue.intValue()}; | |
| 391 | + } | |
| 392 | + } | |
| 393 | + | |
| 394 | + private static int[] encodeInt32(int value, String byteOrder) { | |
| 395 | + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); | |
| 396 | + bytes = reorderBytes(bytes, byteOrder); | |
| 397 | + return bytesToRegisters(bytes); | |
| 398 | + } | |
| 399 | + | |
| 400 | + private static int[] encodeFloat(float value, String byteOrder) { | |
| 401 | + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); | |
| 402 | + bytes = reorderBytes(bytes, byteOrder); | |
| 403 | + return bytesToRegisters(bytes); | |
| 404 | + } | |
| 405 | + | |
| 406 | + private static int[] encodeDouble(double value, String byteOrder) { | |
| 407 | + byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); | |
| 408 | + bytes = reorderBytes(bytes, byteOrder); | |
| 409 | + return bytesToRegisters(bytes); | |
| 410 | + } | |
| 411 | + | |
| 412 | + private static int[] bytesToRegisters(byte[] bytes) { | |
| 413 | + int[] registers = new int[bytes.length / 2]; | |
| 414 | + for (int i = 0; i < registers.length; i++) { | |
| 415 | + registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); | |
| 416 | + } | |
| 417 | + return registers; | |
| 418 | + } | |
| 419 | + | |
| 420 | + @SuppressWarnings("EnhancedSwitchMigration") | |
| 421 | + private static Object formatValue(BigDecimal value, String rawDataType) { | |
| 422 | + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); | |
| 423 | + if (dataTypeEnum == null) { | |
| 424 | + return value; | |
| 425 | + } | |
| 426 | + switch (dataTypeEnum) { | |
| 427 | + case BOOLEAN: | |
| 428 | + return value.intValue() != 0; | |
| 429 | + case INT16: | |
| 430 | + case INT32: | |
| 431 | + return value.intValue(); | |
| 432 | + case UINT16: | |
| 433 | + case UINT32: | |
| 434 | + return value.longValue(); | |
| 435 | + case FLOAT: | |
| 436 | + return value.floatValue(); | |
| 437 | + case DOUBLE: | |
| 438 | + return value.doubleValue(); | |
| 439 | + default: | |
| 440 | + return value; | |
| 441 | + } | |
| 442 | + } | |
| 443 | + | |
| 444 | + // ==================== 帧值提取 ==================== | |
| 196 | 445 | |
| 197 | 446 | /** |
| 198 | 447 | * 从帧中提取寄存器值(FC01-04 读响应) |
| ... | ... | @@ -224,10 +473,6 @@ public class IotModbusUtils { |
| 224 | 473 | } |
| 225 | 474 | } |
| 226 | 475 | |
| 227 | - /** | |
| 228 | - * 提取线圈/离散输入值 | |
| 229 | - * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)] | |
| 230 | - */ | |
| 231 | 476 | private static int[] extractCoilValues(byte[] pdu) { |
| 232 | 477 | if (pdu.length < 2) { |
| 233 | 478 | return null; |
| ... | ... | @@ -241,10 +486,6 @@ public class IotModbusUtils { |
| 241 | 486 | return values; |
| 242 | 487 | } |
| 243 | 488 | |
| 244 | - /** | |
| 245 | - * 提取寄存器值 | |
| 246 | - * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)] | |
| 247 | - */ | |
| 248 | 489 | private static int[] extractRegisterValues(byte[] pdu) { |
| 249 | 490 | if (pdu.length < 2) { |
| 250 | 491 | return null; |
| ... | ... | @@ -258,4 +499,17 @@ public class IotModbusUtils { |
| 258 | 499 | return values; |
| 259 | 500 | } |
| 260 | 501 | |
| 502 | + // ==================== 点位查找 ==================== | |
| 503 | + | |
| 504 | + /** | |
| 505 | + * 查找点位配置 | |
| 506 | + * | |
| 507 | + * @param config 设备 Modbus 配置 | |
| 508 | + * @param identifier 点位标识符 | |
| 509 | + * @return 匹配的点位配置,未找到返回 null | |
| 510 | + */ | |
| 511 | + public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { | |
| 512 | + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); | |
| 513 | + } | |
| 514 | + | |
| 261 | 515 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/IotModbusTcpMasterProtocol.java
| ... | ... | @@ -6,11 +6,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 9 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 9 | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 10 | 11 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 12 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | |
| 13 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; | |
| 14 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamHandler; |
| 15 | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamSubscriber; |
| 16 | 15 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; |
| ... | ... | @@ -70,6 +69,7 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { |
| 70 | 69 | |
| 71 | 70 | private final IotModbusTcpConfigCacheService configCacheService; |
| 72 | 71 | private final IotModbusTcpPollScheduler pollScheduler; |
| 72 | + private final IotDeviceMessageService messageService; | |
| 73 | 73 | |
| 74 | 74 | public IotModbusTcpMasterProtocol(ProtocolProperties properties) { |
| 75 | 75 | IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster(); |
| ... | ... | @@ -87,15 +87,14 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { |
| 87 | 87 | this.configCacheService = new IotModbusTcpConfigCacheService(deviceApi); |
| 88 | 88 | |
| 89 | 89 | // 初始化 Handler |
| 90 | - IotModbusDataConverter dataConverter = new IotModbusDataConverter(); | |
| 91 | - IotModbusTcpClient modbusClient = new IotModbusTcpClient(); | |
| 92 | - IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 93 | - IotModbusTcpUpstreamHandler upstreamHandler = new IotModbusTcpUpstreamHandler(messageService, dataConverter, serverId); | |
| 90 | + this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 91 | + IotDeviceMessageService messageService = this.messageService; | |
| 92 | + IotModbusTcpUpstreamHandler upstreamHandler = new IotModbusTcpUpstreamHandler(messageService, serverId); | |
| 94 | 93 | IotModbusTcpDownstreamHandler downstreamHandler = new IotModbusTcpDownstreamHandler(connectionManager, |
| 95 | - modbusClient, dataConverter, configCacheService); | |
| 94 | + configCacheService); | |
| 96 | 95 | |
| 97 | 96 | // 初始化轮询调度器 |
| 98 | - this.pollScheduler = new IotModbusTcpPollScheduler(vertx, connectionManager, modbusClient, upstreamHandler); | |
| 97 | + this.pollScheduler = new IotModbusTcpPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService); | |
| 99 | 98 | |
| 100 | 99 | // 初始化下行消息订阅者 |
| 101 | 100 | IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); |
| ... | ... | @@ -186,24 +185,54 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { |
| 186 | 185 | */ |
| 187 | 186 | private synchronized void refreshConfig() { |
| 188 | 187 | try { |
| 189 | - // 1. 从 biz 拉取最新配置 | |
| 188 | + // 1. 从 biz 拉取最新配置(API 失败时返回 null) | |
| 190 | 189 | List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig(); |
| 190 | + if (configs == null) { | |
| 191 | + log.warn("[refreshConfig][API 失败,跳过本轮刷新]"); | |
| 192 | + return; | |
| 193 | + } | |
| 191 | 194 | log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); |
| 192 | 195 | |
| 193 | 196 | // 2. 更新连接和轮询任务 |
| 194 | 197 | for (IotModbusDeviceConfigRespDTO config : configs) { |
| 195 | 198 | try { |
| 196 | - // 2.1 确保连接存在 | |
| 199 | + // 2.1 检测是否为首次连接 | |
| 200 | + boolean isNewConnection = connectionManager.getConnection(config.getDeviceId()) == null; | |
| 201 | + // 2.2 确保连接存在 | |
| 197 | 202 | connectionManager.ensureConnection(config); |
| 198 | - // 2.2 更新轮询任务 | |
| 203 | + // 2.3 首次建连成功后发送上线消息 | |
| 204 | + // TODO @AI:在这里判断上线 ,会不会有点奇怪??? | |
| 205 | + if (isNewConnection && connectionManager.getConnection(config.getDeviceId()) != null) { | |
| 206 | + try { | |
| 207 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 208 | + messageService.sendDeviceMessage(onlineMessage, | |
| 209 | + config.getProductKey(), config.getDeviceName(), serverId); | |
| 210 | + } catch (Exception ex) { | |
| 211 | + log.error("[refreshConfig][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex); | |
| 212 | + } | |
| 213 | + } | |
| 214 | + // 2.4 更新轮询任务 | |
| 199 | 215 | pollScheduler.updatePolling(config); |
| 200 | 216 | } catch (Exception e) { |
| 201 | 217 | log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); |
| 202 | 218 | } |
| 203 | 219 | } |
| 204 | 220 | |
| 205 | - // 3. 清理已删除设备的资源 | |
| 221 | + // 3. 清理已删除设备的资源(仅 API 成功时才执行) | |
| 206 | 222 | configCacheService.cleanupRemovedDevices(configs, deviceId -> { |
| 223 | + // 3.1 发送设备下线消息 | |
| 224 | + // TODO @AI:在这里判断上线 ,会不会有点奇怪??? | |
| 225 | + IotModbusDeviceConfigRespDTO removedConfig = configCacheService.getConfig(deviceId); | |
| 226 | + if (removedConfig != null) { | |
| 227 | + try { | |
| 228 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 229 | + messageService.sendDeviceMessage(offlineMessage, | |
| 230 | + removedConfig.getProductKey(), removedConfig.getDeviceName(), serverId); | |
| 231 | + } catch (Exception ex) { | |
| 232 | + log.error("[refreshConfig][发送设备下线消息失败, deviceId={}]", deviceId, ex); | |
| 233 | + } | |
| 234 | + } | |
| 235 | + // 3.2 停止轮询和移除连接 | |
| 207 | 236 | pollScheduler.stopPolling(deviceId); |
| 208 | 237 | connectionManager.removeDevice(deviceId); |
| 209 | 238 | }); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClientUtils.java
| ... | ... | @@ -9,22 +9,24 @@ import com.ghgande.j2mod.modbus.procimg.Register; |
| 9 | 9 | import com.ghgande.j2mod.modbus.procimg.SimpleRegister; |
| 10 | 10 | import com.ghgande.j2mod.modbus.util.BitVector; |
| 11 | 11 | import io.vertx.core.Future; |
| 12 | +import lombok.experimental.UtilityClass; | |
| 12 | 13 | import lombok.extern.slf4j.Slf4j; |
| 13 | 14 | |
| 14 | 15 | import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils.*; |
| 15 | 16 | |
| 16 | -// TODO @AI:感觉它更像一个工具类;但是名字叫 client 很奇怪; | |
| 17 | 17 | /** |
| 18 | - * IoT Modbus TCP 客户端 | |
| 18 | + * IoT Modbus TCP 客户端工具类 | |
| 19 | 19 | * <p> |
| 20 | - * 封装 Modbus 协议读写操作: | |
| 21 | - * 1. 封装 Modbus 读/写操作 | |
| 22 | - * 2. 根据功能码,执行对应的 Modbus 请求 | |
| 20 | + * 封装基于 j2mod 的 Modbus TCP 读写操作: | |
| 21 | + * 1. 根据功能码创建对应的 Modbus 读/写请求 | |
| 22 | + * 2. 通过 {@link IotModbusTcpConnectionManager.ModbusConnection} 执行事务 | |
| 23 | + * 3. 从响应中提取原始值 | |
| 23 | 24 | * |
| 24 | 25 | * @author 芋道源码 |
| 25 | 26 | */ |
| 27 | +@UtilityClass | |
| 26 | 28 | @Slf4j |
| 27 | -public class IotModbusTcpClient { | |
| 29 | +public class IotModbusTcpClientUtils { | |
| 28 | 30 | |
| 29 | 31 | /** |
| 30 | 32 | * 读取 Modbus 数据 |
| ... | ... | @@ -34,7 +36,7 @@ public class IotModbusTcpClient { |
| 34 | 36 | * @param point 点位配置 |
| 35 | 37 | * @return 原始值(int 数组) |
| 36 | 38 | */ |
| 37 | - public Future<int[]> read(IotModbusTcpConnectionManager.ModbusConnection connection, | |
| 39 | + public static Future<int[]> read(IotModbusTcpConnectionManager.ModbusConnection connection, | |
| 38 | 40 | Integer slaveId, |
| 39 | 41 | IotModbusPointRespDTO point) { |
| 40 | 42 | return connection.executeBlocking(tcpConnection -> { |
| ... | ... | @@ -68,7 +70,7 @@ public class IotModbusTcpClient { |
| 68 | 70 | * @param values 要写入的值 |
| 69 | 71 | * @return 是否成功 |
| 70 | 72 | */ |
| 71 | - public Future<Boolean> write(IotModbusTcpConnectionManager.ModbusConnection connection, | |
| 73 | + public static Future<Boolean> write(IotModbusTcpConnectionManager.ModbusConnection connection, | |
| 72 | 74 | Integer slaveId, |
| 73 | 75 | IotModbusPointRespDTO point, |
| 74 | 76 | int[] values) { |
| ... | ... | @@ -98,7 +100,7 @@ public class IotModbusTcpClient { |
| 98 | 100 | * 创建读取请求 |
| 99 | 101 | */ |
| 100 | 102 | @SuppressWarnings("EnhancedSwitchMigration") |
| 101 | - private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { | |
| 103 | + private static ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { | |
| 102 | 104 | switch (functionCode) { |
| 103 | 105 | case FC_READ_COILS: |
| 104 | 106 | return new ReadCoilsRequest(address, count); |
| ... | ... | @@ -117,7 +119,7 @@ public class IotModbusTcpClient { |
| 117 | 119 | * 创建写入请求 |
| 118 | 120 | */ |
| 119 | 121 | @SuppressWarnings("EnhancedSwitchMigration") |
| 120 | - private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { | |
| 122 | + private static ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { | |
| 121 | 123 | switch (functionCode) { |
| 122 | 124 | case FC_READ_COILS: // 写线圈(使用功能码 5 或 15) |
| 123 | 125 | if (count == 1) { |
| ... | ... | @@ -151,7 +153,7 @@ public class IotModbusTcpClient { |
| 151 | 153 | * 从响应中提取值 |
| 152 | 154 | */ |
| 153 | 155 | @SuppressWarnings("EnhancedSwitchMigration") |
| 154 | - private int[] extractValues(ModbusResponse response, Integer functionCode) { | |
| 156 | + private static int[] extractValues(ModbusResponse response, Integer functionCode) { | |
| 155 | 157 | switch (functionCode) { |
| 156 | 158 | case FC_READ_COILS: |
| 157 | 159 | ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java
| ... | ... | @@ -4,9 +4,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | |
| 8 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClientUtils; | |
| 10 | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; |
| 11 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; |
| 12 | 11 | import lombok.RequiredArgsConstructor; |
| ... | ... | @@ -28,8 +27,6 @@ import java.util.Map; |
| 28 | 27 | public class IotModbusTcpDownstreamHandler { |
| 29 | 28 | |
| 30 | 29 | private final IotModbusTcpConnectionManager connectionManager; |
| 31 | - private final IotModbusTcpClient modbusClient; | |
| 32 | - private final IotModbusDataConverter dataConverter; | |
| 33 | 30 | private final IotModbusTcpConfigCacheService configCacheService; |
| 34 | 31 | |
| 35 | 32 | /** |
| ... | ... | @@ -94,9 +91,9 @@ public class IotModbusTcpDownstreamHandler { |
| 94 | 91 | } |
| 95 | 92 | |
| 96 | 93 | // 2.1 转换属性值为原始值 |
| 97 | - int[] rawValues = dataConverter.convertToRawValues(value, point); | |
| 94 | + int[] rawValues = IotModbusUtils.convertToRawValues(value, point); | |
| 98 | 95 | // 2.2 执行 Modbus 写入 |
| 99 | - modbusClient.write(connection, slaveId, point, rawValues) | |
| 96 | + IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues) | |
| 100 | 97 | .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", |
| 101 | 98 | config.getDeviceId(), point.getIdentifier(), value)) |
| 102 | 99 | .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java
| ... | ... | @@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; | |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 10 | 10 | import lombok.extern.slf4j.Slf4j; |
| 11 | 11 | |
| ... | ... | @@ -20,14 +20,11 @@ import java.util.Map; |
| 20 | 20 | public class IotModbusTcpUpstreamHandler { |
| 21 | 21 | |
| 22 | 22 | private final IotDeviceMessageService messageService; |
| 23 | - private final IotModbusDataConverter dataConverter; | |
| 24 | 23 | private final String serverId; |
| 25 | 24 | |
| 26 | 25 | public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService, |
| 27 | - IotModbusDataConverter dataConverter, | |
| 28 | 26 | String serverId) { |
| 29 | 27 | this.messageService = messageService; |
| 30 | - this.dataConverter = dataConverter; | |
| 31 | 28 | this.serverId = serverId; |
| 32 | 29 | } |
| 33 | 30 | |
| ... | ... | @@ -43,7 +40,7 @@ public class IotModbusTcpUpstreamHandler { |
| 43 | 40 | int[] rawValue) { |
| 44 | 41 | try { |
| 45 | 42 | // 1.1 转换原始值为物模型属性值 |
| 46 | - Object convertedValue = dataConverter.convertToPropertyValue(rawValue, point); | |
| 43 | + Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValue, point); | |
| 47 | 44 | log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", |
| 48 | 45 | config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); |
| 49 | 46 | // 1.2 构造属性上报消息 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConfigCacheService.java
| ... | ... | @@ -38,16 +38,13 @@ public class IotModbusTcpConfigCacheService { |
| 38 | 38 | /** |
| 39 | 39 | * 刷新配置 |
| 40 | 40 | * |
| 41 | - * @return 最新的配置列表 | |
| 41 | + * @return 最新的配置列表;API 失败时返回 null(调用方应跳过 cleanup) | |
| 42 | 42 | */ |
| 43 | 43 | public List<IotModbusDeviceConfigRespDTO> refreshConfig() { |
| 44 | 44 | try { |
| 45 | 45 | // 1. 从远程获取配置 |
| 46 | 46 | CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs(); |
| 47 | - if (result == null || !result.isSuccess() || result.getData() == null) { | |
| 48 | - log.warn("[refreshConfig][获取 Modbus 配置失败: {}]", result); | |
| 49 | - return new ArrayList<>(configCache.values()); | |
| 50 | - } | |
| 47 | + result.checkError(); | |
| 51 | 48 | List<IotModbusDeviceConfigRespDTO> configs = result.getData(); |
| 52 | 49 | |
| 53 | 50 | // 2. 更新缓存(注意:不在这里更新 knownDeviceIds,由 cleanupRemovedDevices 统一管理) |
| ... | ... | @@ -57,7 +54,7 @@ public class IotModbusTcpConfigCacheService { |
| 57 | 54 | return configs; |
| 58 | 55 | } catch (Exception e) { |
| 59 | 56 | log.error("[refreshConfig][刷新配置失败]", e); |
| 60 | - return new ArrayList<>(configCache.values()); | |
| 57 | + return null; | |
| 61 | 58 | } |
| 62 | 59 | } |
| 63 | 60 | |
| ... | ... | @@ -84,11 +81,11 @@ public class IotModbusTcpConfigCacheService { |
| 84 | 81 | Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds); |
| 85 | 82 | removedDeviceIds.removeAll(currentDeviceIds); |
| 86 | 83 | |
| 87 | - // 2. 清理已删除设备 | |
| 84 | + // 2. 清理已删除设备(先执行 cleanupAction,再从缓存移除,保证 action 中仍可获取 config) | |
| 88 | 85 | for (Long deviceId : removedDeviceIds) { |
| 89 | 86 | log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); |
| 90 | - configCache.remove(deviceId); | |
| 91 | 87 | cleanupAction.accept(deviceId); |
| 88 | + configCache.remove(deviceId); | |
| 92 | 89 | } |
| 93 | 90 | |
| 94 | 91 | // 3. 更新已知设备 ID 集合为当前有效的设备 ID | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpConnectionManager.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.ObjUtil; | |
| 3 | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 4 | 5 | import com.ghgande.j2mod.modbus.net.TCPMasterConnection; |
| 5 | 6 | import io.vertx.core.Context; |
| ... | ... | @@ -50,8 +51,15 @@ public class IotModbusTcpConnectionManager { |
| 50 | 51 | * @param config 设备配置 |
| 51 | 52 | */ |
| 52 | 53 | public void ensureConnection(IotModbusDeviceConfigRespDTO config) { |
| 53 | - // 1. 记录设备与连接的关系 | |
| 54 | + // 1.1 检查设备是否切换了 IP/端口,若是则先清理旧连接 | |
| 54 | 55 | String connectionKey = buildConnectionKey(config.getIp(), config.getPort()); |
| 56 | + String oldConnectionKey = deviceConnectionMap.get(config.getDeviceId()); | |
| 57 | + if (oldConnectionKey != null && ObjUtil.notEqual(oldConnectionKey, connectionKey)) { | |
| 58 | + log.info("[ensureConnection][设备 {} IP/端口变更: {} -> {}, 清理旧连接]", | |
| 59 | + config.getDeviceId(), oldConnectionKey, connectionKey); | |
| 60 | + removeDevice(config.getDeviceId()); | |
| 61 | + } | |
| 62 | + // 1.2 记录设备与连接的关系 | |
| 55 | 63 | deviceConnectionMap.put(config.getDeviceId(), connectionKey); |
| 56 | 64 | |
| 57 | 65 | // 2. 情况一:连接已存在,添加设备引用 |
| ... | ... | @@ -68,8 +76,15 @@ public class IotModbusTcpConnectionManager { |
| 68 | 76 | log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey); |
| 69 | 77 | return; |
| 70 | 78 | } |
| 71 | - // 3.2 创建新连接 | |
| 79 | + // 3.2 double-check:拿到锁后再次检查,避免并发创建重复连接 | |
| 72 | 80 | try { |
| 81 | + connection = connectionPool.get(connectionKey); | |
| 82 | + if (connection != null) { | |
| 83 | + connection.addDevice(config.getDeviceId(), config.getSlaveId()); | |
| 84 | + lock.unlock(); | |
| 85 | + return; | |
| 86 | + } | |
| 87 | + // 3.3 创建新连接 | |
| 73 | 88 | connection = createConnection(config, lock); |
| 74 | 89 | connectionPool.put(connectionKey, connection); |
| 75 | 90 | log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); |
| ... | ... | @@ -154,10 +169,10 @@ public class IotModbusTcpConnectionManager { |
| 154 | 169 | if (connection.getTcpConnection() != null) { |
| 155 | 170 | connection.getTcpConnection().close(); |
| 156 | 171 | } |
| 157 | - // 安全释放锁:先检查锁存在且被锁定,再检查是否当前线程持有 | |
| 172 | + // 强制解锁,避免死锁(正常情况下应该不会发生锁未释放的情况) | |
| 158 | 173 | RLock lock = connection.getLock(); |
| 159 | - if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { | |
| 160 | - lock.unlock(); | |
| 174 | + if (lock != null && lock.isLocked()) { | |
| 175 | + lock.forceUnlock(); | |
| 161 | 176 | } |
| 162 | 177 | log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); |
| 163 | 178 | } catch (Exception e) { | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java
| ... | ... | @@ -3,33 +3,37 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClientUtils; | |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; |
| 8 | 8 | import io.vertx.core.Vertx; |
| 9 | 9 | import lombok.AllArgsConstructor; |
| 10 | 10 | import lombok.Data; |
| 11 | -import lombok.RequiredArgsConstructor; | |
| 12 | 11 | import lombok.extern.slf4j.Slf4j; |
| 13 | 12 | |
| 14 | 13 | import java.util.*; |
| 15 | 14 | import java.util.concurrent.ConcurrentHashMap; |
| 15 | +import java.util.concurrent.ConcurrentLinkedQueue; | |
| 16 | 16 | |
| 17 | 17 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| 18 | 18 | |
| 19 | 19 | // TODO @AI:类的命名上,要体现上 master。其它类似 /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 也要! |
| 20 | 20 | /** |
| 21 | - * IoT Modbus TCP 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 | |
| 21 | + * IoT Modbus TCP Master 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果 | |
| 22 | 22 | * |
| 23 | 23 | * @author 芋道源码 |
| 24 | 24 | */ |
| 25 | -@RequiredArgsConstructor | |
| 26 | 25 | @Slf4j |
| 27 | 26 | public class IotModbusTcpPollScheduler { |
| 28 | 27 | |
| 29 | 28 | private final Vertx vertx; |
| 30 | 29 | private final IotModbusTcpConnectionManager connectionManager; |
| 31 | - private final IotModbusTcpClient modbusClient; | |
| 32 | 30 | private final IotModbusTcpUpstreamHandler upstreamHandler; |
| 31 | + private final IotModbusTcpConfigCacheService configCacheService; | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 | |
| 35 | + */ | |
| 36 | + private static final long MIN_REQUEST_INTERVAL = 100; | |
| 33 | 37 | |
| 34 | 38 | /** |
| 35 | 39 | * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) |
| ... | ... | @@ -37,6 +41,33 @@ public class IotModbusTcpPollScheduler { |
| 37 | 41 | private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>(); |
| 38 | 42 | |
| 39 | 43 | /** |
| 44 | + * per-device 请求队列:deviceId -> 待执行请求队列 | |
| 45 | + */ | |
| 46 | + private final Map<Long, Queue<Runnable>> deviceRequestQueues = new ConcurrentHashMap<>(); | |
| 47 | + | |
| 48 | + /** | |
| 49 | + * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs | |
| 50 | + */ | |
| 51 | + private final Map<Long, Long> deviceLastRequestTime = new ConcurrentHashMap<>(); | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待 | |
| 55 | + */ | |
| 56 | + private final Map<Long, Boolean> deviceDelayTimerActive = new ConcurrentHashMap<>(); | |
| 57 | + | |
| 58 | + public IotModbusTcpPollScheduler(Vertx vertx, | |
| 59 | + IotModbusTcpConnectionManager connectionManager, | |
| 60 | + IotModbusTcpUpstreamHandler upstreamHandler, | |
| 61 | + IotModbusTcpConfigCacheService configCacheService) { | |
| 62 | + this.vertx = vertx; | |
| 63 | + this.connectionManager = connectionManager; | |
| 64 | + this.upstreamHandler = upstreamHandler; | |
| 65 | + this.configCacheService = configCacheService; | |
| 66 | + } | |
| 67 | + | |
| 68 | + // ========== 点位定时器 ========== | |
| 69 | + | |
| 70 | + /** | |
| 40 | 71 | * 点位定时器信息 |
| 41 | 72 | */ |
| 42 | 73 | @Data |
| ... | ... | @@ -54,20 +85,22 @@ public class IotModbusTcpPollScheduler { |
| 54 | 85 | |
| 55 | 86 | } |
| 56 | 87 | |
| 88 | + // ========== 轮询管理 ========== | |
| 89 | + | |
| 57 | 90 | /** |
| 58 | 91 | * 更新轮询任务(增量更新) |
| 59 | 92 | * |
| 60 | 93 | * 1. 【删除】点位:停止对应的轮询定时器 |
| 61 | 94 | * 2. 【新增】点位:创建对应的轮询定时器 |
| 62 | 95 | * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 |
| 63 | - * 4. 其他属性变化(包括未变化的):不处理(下次轮询时自动使用新配置) | |
| 96 | + * 4. 其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point) | |
| 64 | 97 | */ |
| 65 | 98 | public void updatePolling(IotModbusDeviceConfigRespDTO config) { |
| 66 | 99 | Long deviceId = config.getDeviceId(); |
| 67 | 100 | List<IotModbusPointRespDTO> newPoints = config.getPoints(); |
| 68 | 101 | Map<Long, PointTimerInfo> currentTimers = devicePointTimers |
| 69 | 102 | .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); |
| 70 | - // 1.1 计算新配置(包括新增和修改的点位)中的点位 ID 集合 | |
| 103 | + // 1.1 计算新配置中的点位 ID 集合 | |
| 71 | 104 | Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); |
| 72 | 105 | // 1.2 计算删除的点位 ID 集合 |
| 73 | 106 | Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet()); |
| ... | ... | @@ -92,7 +125,7 @@ public class IotModbusTcpPollScheduler { |
| 92 | 125 | PointTimerInfo existingTimer = currentTimers.get(pointId); |
| 93 | 126 | // 3.1 新增点位:创建定时器 |
| 94 | 127 | if (existingTimer == null) { |
| 95 | - Long timerId = createPollTimer(config, point); | |
| 128 | + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); | |
| 96 | 129 | if (timerId != null) { |
| 97 | 130 | currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); |
| 98 | 131 | log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", |
| ... | ... | @@ -101,7 +134,7 @@ public class IotModbusTcpPollScheduler { |
| 101 | 134 | } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { |
| 102 | 135 | // 3.2 pollInterval 变化:重建定时器 |
| 103 | 136 | vertx.cancelTimer(existingTimer.getTimerId()); |
| 104 | - Long timerId = createPollTimer(config, point); | |
| 137 | + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); | |
| 105 | 138 | if (timerId != null) { |
| 106 | 139 | currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); |
| 107 | 140 | log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", |
| ... | ... | @@ -110,51 +143,136 @@ public class IotModbusTcpPollScheduler { |
| 110 | 143 | currentTimers.remove(pointId); |
| 111 | 144 | } |
| 112 | 145 | } |
| 113 | - // 3.3 其他属性变化:不处理(下次轮询时自动使用新配置) | |
| 146 | + // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置 | |
| 114 | 147 | } |
| 115 | 148 | } |
| 116 | 149 | |
| 117 | 150 | /** |
| 118 | 151 | * 创建轮询定时器 |
| 152 | + * <p> | |
| 153 | + * 闭包只捕获 deviceId 和 pointId,运行时从 configCache 获取最新配置,避免旧快照问题。 | |
| 119 | 154 | */ |
| 120 | - private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { | |
| 121 | - if (point.getPollInterval() == null || point.getPollInterval() <= 0) { | |
| 155 | + private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) { | |
| 156 | + if (pollInterval == null || pollInterval <= 0) { | |
| 122 | 157 | return null; |
| 123 | 158 | } |
| 124 | - return vertx.setPeriodic(point.getPollInterval(), timerId -> { | |
| 159 | + return vertx.setPeriodic(pollInterval, timerId -> { | |
| 125 | 160 | try { |
| 126 | - pollPoint(config, point); | |
| 161 | + submitPollRequest(deviceId, pointId); | |
| 127 | 162 | } catch (Exception e) { |
| 128 | - log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]", | |
| 129 | - config.getDeviceId(), point.getIdentifier(), e); | |
| 163 | + log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e); | |
| 130 | 164 | } |
| 131 | 165 | }); |
| 132 | 166 | } |
| 133 | 167 | |
| 168 | + // ========== 请求队列(per-device 限速) ========== | |
| 169 | + | |
| 170 | + /** | |
| 171 | + * 提交轮询请求到设备请求队列(保证同设备请求间隔) | |
| 172 | + */ | |
| 173 | + private void submitPollRequest(Long deviceId, Long pointId) { | |
| 174 | + Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); | |
| 175 | + queue.offer(() -> pollPoint(deviceId, pointId)); | |
| 176 | + processDeviceQueue(deviceId); | |
| 177 | + } | |
| 178 | + | |
| 179 | + /** | |
| 180 | + * 处理设备请求队列 | |
| 181 | + */ | |
| 182 | + private void processDeviceQueue(Long deviceId) { | |
| 183 | + Queue<Runnable> queue = deviceRequestQueues.get(deviceId); | |
| 184 | + if (CollUtil.isEmpty(queue)) { | |
| 185 | + return; | |
| 186 | + } | |
| 187 | + // 检查是否已有延迟 timer 在等待 | |
| 188 | + if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) { | |
| 189 | + return; | |
| 190 | + } | |
| 191 | + long now = System.currentTimeMillis(); | |
| 192 | + long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L); | |
| 193 | + long elapsed = now - lastTime; | |
| 194 | + if (elapsed >= MIN_REQUEST_INTERVAL) { | |
| 195 | + // 满足间隔要求,立即执行 | |
| 196 | + Runnable task = queue.poll(); | |
| 197 | + if (task != null) { | |
| 198 | + deviceLastRequestTime.put(deviceId, now); | |
| 199 | + task.run(); | |
| 200 | + // 继续处理队列中的下一个(如果有的话,需要延迟) | |
| 201 | + if (!queue.isEmpty()) { | |
| 202 | + scheduleNextRequest(deviceId); | |
| 203 | + } | |
| 204 | + } | |
| 205 | + } else { | |
| 206 | + // 需要延迟 | |
| 207 | + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed); | |
| 208 | + } | |
| 209 | + } | |
| 210 | + | |
| 211 | + private void scheduleNextRequest(Long deviceId) { | |
| 212 | + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL); | |
| 213 | + } | |
| 214 | + | |
| 215 | + private void scheduleNextRequest(Long deviceId, long delayMs) { | |
| 216 | + deviceDelayTimerActive.put(deviceId, true); | |
| 217 | + vertx.setTimer(delayMs, id -> { | |
| 218 | + deviceDelayTimerActive.put(deviceId, false); | |
| 219 | + Queue<Runnable> queue = deviceRequestQueues.get(deviceId); | |
| 220 | + if (CollUtil.isEmpty(queue)) { | |
| 221 | + Runnable task = queue.poll(); | |
| 222 | + if (task != null) { | |
| 223 | + deviceLastRequestTime.put(deviceId, System.currentTimeMillis()); | |
| 224 | + task.run(); | |
| 225 | + } | |
| 226 | + // 继续处理 | |
| 227 | + if (queue != null && !queue.isEmpty()) { | |
| 228 | + scheduleNextRequest(deviceId); | |
| 229 | + } | |
| 230 | + } | |
| 231 | + }); | |
| 232 | + } | |
| 233 | + | |
| 234 | + // ========== 轮询执行 ========== | |
| 235 | + | |
| 134 | 236 | /** |
| 135 | 237 | * 轮询单个点位 |
| 136 | 238 | */ |
| 137 | - private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { | |
| 138 | - // 1.1 获取连接 | |
| 139 | - IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId()); | |
| 239 | + private void pollPoint(Long deviceId, Long pointId) { | |
| 240 | + // 1.1 从 configCache 获取最新配置 | |
| 241 | + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); | |
| 242 | + if (config == null || CollUtil.isEmpty(config.getPoints())) { | |
| 243 | + log.warn("[pollPoint][设备 {} 没有配置]", deviceId); | |
| 244 | + return; | |
| 245 | + } | |
| 246 | + // 1.2 查找点位 | |
| 247 | + // TODO @AI:是不是这里,可以抽到 IotModbusUtils 里?感觉应该有几个地方需要的; | |
| 248 | + IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); | |
| 249 | + if (point == null) { | |
| 250 | + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); | |
| 251 | + return; | |
| 252 | + } | |
| 253 | + | |
| 254 | + // 2.1 获取连接 | |
| 255 | + IotModbusTcpConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId); | |
| 140 | 256 | if (connection == null) { |
| 141 | - log.warn("[pollPoint][设备 {} 没有连接]", config.getDeviceId()); | |
| 257 | + log.warn("[pollPoint][设备 {} 没有连接]", deviceId); | |
| 142 | 258 | return; |
| 143 | 259 | } |
| 144 | - // 1.2 获取 slave ID | |
| 145 | - Integer slaveId = connectionManager.getSlaveId(config.getDeviceId()); | |
| 260 | + // 2.2 获取 slave ID | |
| 261 | + Integer slaveId = connectionManager.getSlaveId(deviceId); | |
| 146 | 262 | if (slaveId == null) { |
| 147 | - log.warn("[pollPoint][设备 {} 没有 slaveId]", config.getDeviceId()); | |
| 263 | + log.warn("[pollPoint][设备 {} 没有 slaveId]", deviceId); | |
| 148 | 264 | return; |
| 149 | 265 | } |
| 150 | 266 | |
| 151 | - // 2. 执行 Modbus 读取 | |
| 152 | - modbusClient.read(connection, slaveId, point) | |
| 267 | + // 3. 执行 Modbus 读取 | |
| 268 | + IotModbusTcpClientUtils.read(connection, slaveId, point) | |
| 153 | 269 | .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) |
| 154 | 270 | .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", |
| 155 | - config.getDeviceId(), point.getIdentifier(), e)); | |
| 271 | + deviceId, point.getIdentifier(), e)); | |
| 156 | 272 | } |
| 157 | 273 | |
| 274 | + // ========== 停止 ========== | |
| 275 | + | |
| 158 | 276 | /** |
| 159 | 277 | * 停止设备的轮询 |
| 160 | 278 | */ |
| ... | ... | @@ -166,6 +284,10 @@ public class IotModbusTcpPollScheduler { |
| 166 | 284 | for (PointTimerInfo timerInfo : timers.values()) { |
| 167 | 285 | vertx.cancelTimer(timerInfo.getTimerId()); |
| 168 | 286 | } |
| 287 | + // 清理请求队列 | |
| 288 | + deviceRequestQueues.remove(deviceId); | |
| 289 | + deviceLastRequestTime.remove(deviceId); | |
| 290 | + deviceDelayTimerActive.remove(deviceId); | |
| 169 | 291 | log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); |
| 170 | 292 | } |
| 171 | 293 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java
| ... | ... | @@ -41,4 +41,10 @@ public class IotModbusTcpSlaveConfig { |
| 41 | 41 | @NotNull(message = "请求清理间隔不能为空") |
| 42 | 42 | private Integer requestCleanupInterval = 10000; |
| 43 | 43 | |
| 44 | + // TODO @AI:可以去掉这个开关,因为本身就是模拟的,稍后我自己也会手动或者让你去掉(听我指令!) | |
| 45 | + /** | |
| 46 | + * 是否启用 Mock 测试数据(仅开发/测试环境使用,线上务必关闭) | |
| 47 | + */ | |
| 48 | + private Boolean mockEnabled = false; | |
| 49 | + | |
| 44 | 50 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveProtocol.java
| ... | ... | @@ -7,10 +7,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 10 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 10 | 11 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; |
| 12 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 13 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | |
| 14 | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; |
| 15 | 15 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; |
| 16 | 16 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; |
| ... | ... | @@ -34,6 +34,7 @@ import lombok.extern.slf4j.Slf4j; |
| 34 | 34 | import java.util.List; |
| 35 | 35 | import java.util.Set; |
| 36 | 36 | import java.util.concurrent.TimeUnit; |
| 37 | +import java.util.concurrent.atomic.AtomicInteger; | |
| 37 | 38 | |
| 38 | 39 | /** |
| 39 | 40 | * IoT 网关 Modbus TCP Slave 协议 |
| ... | ... | @@ -92,6 +93,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { |
| 92 | 93 | private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; |
| 93 | 94 | private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; |
| 94 | 95 | private final IotModbusTcpSlavePollScheduler pollScheduler; |
| 96 | + private final IotDeviceMessageService messageService; | |
| 95 | 97 | |
| 96 | 98 | public IotModbusTcpSlaveProtocol(ProtocolProperties properties) { |
| 97 | 99 | this.slaveConfig = properties.getModbusTcpSlave(); |
| ... | ... | @@ -112,24 +114,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { |
| 112 | 114 | this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode()); |
| 113 | 115 | this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode()); |
| 114 | 116 | |
| 117 | + // 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突) | |
| 118 | + AtomicInteger transactionIdCounter = new AtomicInteger(0); | |
| 119 | + | |
| 115 | 120 | // 初始化轮询调度器 |
| 116 | 121 | this.pollScheduler = new IotModbusTcpSlavePollScheduler( |
| 117 | 122 | vertx, connectionManager, frameEncoder, pendingRequestManager, |
| 118 | - slaveConfig.getRequestTimeout()); | |
| 123 | + slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService); | |
| 119 | 124 | |
| 120 | 125 | // 初始化 Handler |
| 121 | - IotModbusDataConverter dataConverter = new IotModbusDataConverter(); | |
| 122 | - IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 126 | + this.messageService = SpringUtil.getBean(IotDeviceMessageService.class); | |
| 127 | + IotDeviceMessageService messageService = this.messageService; | |
| 123 | 128 | IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); |
| 124 | 129 | this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( |
| 125 | - deviceApi, messageService, dataConverter, frameEncoder, | |
| 130 | + deviceApi, messageService, frameEncoder, | |
| 126 | 131 | connectionManager, configCacheService, pendingRequestManager, |
| 127 | 132 | pollScheduler, deviceService, serverId); |
| 128 | 133 | |
| 129 | 134 | // 初始化下行消息订阅者 |
| 130 | 135 | IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); |
| 131 | 136 | IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( |
| 132 | - connectionManager, configCacheService, dataConverter, frameEncoder); | |
| 137 | + connectionManager, configCacheService, frameEncoder, transactionIdCounter); | |
| 133 | 138 | this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( |
| 134 | 139 | this, downstreamHandler, messageBus); |
| 135 | 140 | } |
| ... | ... | @@ -284,6 +289,13 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { |
| 284 | 289 | pollScheduler.stopPolling(info.getDeviceId()); |
| 285 | 290 | pendingRequestManager.removeDevice(info.getDeviceId()); |
| 286 | 291 | configCacheService.removeConfig(info.getDeviceId()); |
| 292 | + // 发送设备下线消息 | |
| 293 | + try { | |
| 294 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 295 | + messageService.sendDeviceMessage(offlineMessage, info.getProductKey(), info.getDeviceName(), serverId); | |
| 296 | + } catch (Exception ex) { | |
| 297 | + log.error("[handleConnection][发送设备下线消息失败, deviceId={}]", info.getDeviceId(), ex); | |
| 298 | + } | |
| 287 | 299 | log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", |
| 288 | 300 | info.getDeviceId(), socket.remoteAddress()); |
| 289 | 301 | }); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java
| ... | ... | @@ -8,9 +8,7 @@ import lombok.extern.slf4j.Slf4j; |
| 8 | 8 | import java.nio.charset.StandardCharsets; |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | - * IoT Modbus 帧编码器 | |
| 12 | - * <p> | |
| 13 | - * 负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。 | |
| 11 | + * IoT Modbus 帧编码器:负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。 | |
| 14 | 12 | * |
| 15 | 13 | * @author 芋道源码 |
| 16 | 14 | */ |
| ... | ... | @@ -30,11 +28,11 @@ public class IotModbusFrameEncoder { |
| 30 | 28 | * @param startAddress 起始寄存器地址 |
| 31 | 29 | * @param quantity 寄存器数量 |
| 32 | 30 | * @param format 帧格式 |
| 33 | - * @param transactionId 事务 ID(TCP 模式下使用) | |
| 31 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 34 | 32 | * @return 编码后的字节数组 |
| 35 | 33 | */ |
| 36 | 34 | public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity, |
| 37 | - IotModbusFrameFormatEnum format, int transactionId) { | |
| 35 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 38 | 36 | // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)] |
| 39 | 37 | byte[] pdu = new byte[5]; |
| 40 | 38 | pdu[0] = (byte) functionCode; |
| ... | ... | @@ -48,16 +46,23 @@ public class IotModbusFrameEncoder { |
| 48 | 46 | /** |
| 49 | 47 | * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) |
| 50 | 48 | * |
| 49 | + * DONE @AI:【from codex】【高】FC05 写线圈时,value 已转换为 Modbus 标准值(非0 → 0xFF00,0 → 0x0000); | |
| 50 | + * 新增 encodeWriteMultipleCoilsRequest 方法用于 FC15 编码(按 bit 打包)。 | |
| 51 | + * | |
| 51 | 52 | * @param slaveId 从站地址 |
| 52 | 53 | * @param functionCode 功能码 |
| 53 | 54 | * @param address 寄存器地址 |
| 54 | 55 | * @param value 值 |
| 55 | 56 | * @param format 帧格式 |
| 56 | - * @param transactionId 事务 ID | |
| 57 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 57 | 58 | * @return 编码后的字节数组 |
| 58 | 59 | */ |
| 59 | 60 | public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value, |
| 60 | - IotModbusFrameFormatEnum format, int transactionId) { | |
| 61 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 62 | + // FC05 单写线圈:Modbus 标准要求 value 为 0xFF00(ON)或 0x0000(OFF) | |
| 63 | + if (functionCode == IotModbusUtils.FC_WRITE_SINGLE_COIL) { | |
| 64 | + value = (value != 0) ? 0xFF00 : 0x0000; | |
| 65 | + } | |
| 61 | 66 | // PDU: [FC(1)] [Address(2)] [Value(2)] |
| 62 | 67 | byte[] pdu = new byte[5]; |
| 63 | 68 | pdu[0] = (byte) functionCode; |
| ... | ... | @@ -75,11 +80,11 @@ public class IotModbusFrameEncoder { |
| 75 | 80 | * @param address 起始地址 |
| 76 | 81 | * @param values 值数组 |
| 77 | 82 | * @param format 帧格式 |
| 78 | - * @param transactionId 事务 ID | |
| 83 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 79 | 84 | * @return 编码后的字节数组 |
| 80 | 85 | */ |
| 81 | 86 | public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values, |
| 82 | - IotModbusFrameFormatEnum format, int transactionId) { | |
| 87 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 83 | 88 | // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)] |
| 84 | 89 | int quantity = values.length; |
| 85 | 90 | int byteCount = quantity * 2; |
| ... | ... | @@ -98,16 +103,49 @@ public class IotModbusFrameEncoder { |
| 98 | 103 | } |
| 99 | 104 | |
| 100 | 105 | /** |
| 106 | + * 编码写多个线圈请求(FC15) | |
| 107 | + * <p> | |
| 108 | + * 按 Modbus FC15 标准,线圈值按 bit 打包(每个 byte 包含 8 个线圈状态)。 | |
| 109 | + * | |
| 110 | + * @param slaveId 从站地址 | |
| 111 | + * @param address 起始地址 | |
| 112 | + * @param values 线圈值数组(int[],非0 表示 ON,0 表示 OFF) | |
| 113 | + * @param format 帧格式 | |
| 114 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 115 | + * @return 编码后的字节数组 | |
| 116 | + */ | |
| 117 | + public byte[] encodeWriteMultipleCoilsRequest(int slaveId, int address, int[] values, | |
| 118 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 119 | + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [CoilValues(N)] | |
| 120 | + int quantity = values.length; | |
| 121 | + int byteCount = (quantity + 7) / 8; // 向上取整 | |
| 122 | + byte[] pdu = new byte[6 + byteCount]; | |
| 123 | + pdu[0] = (byte) IotModbusUtils.FC_WRITE_MULTIPLE_COILS; // FC15 | |
| 124 | + pdu[1] = (byte) ((address >> 8) & 0xFF); | |
| 125 | + pdu[2] = (byte) (address & 0xFF); | |
| 126 | + pdu[3] = (byte) ((quantity >> 8) & 0xFF); | |
| 127 | + pdu[4] = (byte) (quantity & 0xFF); | |
| 128 | + pdu[5] = (byte) byteCount; | |
| 129 | + // 按 bit 打包:每个 byte 的 bit0 对应最低地址的线圈 | |
| 130 | + for (int i = 0; i < quantity; i++) { | |
| 131 | + if (values[i] != 0) { | |
| 132 | + pdu[6 + i / 8] |= (byte) (1 << (i % 8)); | |
| 133 | + } | |
| 134 | + } | |
| 135 | + return wrapFrame(slaveId, pdu, format, transactionId); | |
| 136 | + } | |
| 137 | + | |
| 138 | + /** | |
| 101 | 139 | * 编码自定义功能码帧(认证响应等) |
| 102 | 140 | * |
| 103 | 141 | * @param slaveId 从站地址 |
| 104 | 142 | * @param jsonData JSON 数据 |
| 105 | 143 | * @param format 帧格式 |
| 106 | - * @param transactionId 事务 ID | |
| 144 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null) | |
| 107 | 145 | * @return 编码后的字节数组 |
| 108 | 146 | */ |
| 109 | 147 | public byte[] encodeCustomFrame(int slaveId, String jsonData, |
| 110 | - IotModbusFrameFormatEnum format, int transactionId) { | |
| 148 | + IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 111 | 149 | byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8); |
| 112 | 150 | // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)] |
| 113 | 151 | byte[] pdu = new byte[2 + jsonBytes.length]; |
| ... | ... | @@ -125,12 +163,12 @@ public class IotModbusFrameEncoder { |
| 125 | 163 | * @param slaveId 从站地址 |
| 126 | 164 | * @param pdu PDU 数据(含 functionCode) |
| 127 | 165 | * @param format 帧格式 |
| 128 | - * @param transactionId 事务 ID(TCP 模式下使用) | |
| 166 | + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式可为 null) | |
| 129 | 167 | * @return 完整帧字节数组 |
| 130 | 168 | */ |
| 131 | - private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, int transactionId) { | |
| 169 | + private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, Integer transactionId) { | |
| 132 | 170 | if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { |
| 133 | - return wrapTcpFrame(slaveId, pdu, transactionId); | |
| 171 | + return wrapTcpFrame(slaveId, pdu, transactionId != null ? transactionId : 0); | |
| 134 | 172 | } else { |
| 135 | 173 | return wrapRtuFrame(slaveId, pdu); |
| 136 | 174 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
| ... | ... | @@ -7,13 +7,11 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | |
| 11 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; |
| 12 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; |
| 13 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; |
| 14 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; |
| 15 | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; |
| 16 | -import lombok.RequiredArgsConstructor; | |
| 17 | 15 | import lombok.extern.slf4j.Slf4j; |
| 18 | 16 | |
| 19 | 17 | import java.util.Map; |
| ... | ... | @@ -28,19 +26,27 @@ import java.util.concurrent.atomic.AtomicInteger; |
| 28 | 26 | * |
| 29 | 27 | * @author 芋道源码 |
| 30 | 28 | */ |
| 31 | -@RequiredArgsConstructor | |
| 32 | 29 | @Slf4j |
| 33 | 30 | public class IotModbusTcpSlaveDownstreamHandler { |
| 34 | 31 | |
| 35 | 32 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 36 | 33 | private final IotModbusTcpSlaveConfigCacheService configCacheService; |
| 37 | - private final IotModbusDataConverter dataConverter; | |
| 38 | 34 | private final IotModbusFrameEncoder frameEncoder; |
| 39 | 35 | |
| 40 | 36 | /** |
| 41 | - * TCP 事务 ID 自增器 | |
| 37 | + * TCP 事务 ID 自增器(与 PollScheduler 共享) | |
| 42 | 38 | */ |
| 43 | - private final AtomicInteger transactionIdCounter = new AtomicInteger(0); | |
| 39 | + private final AtomicInteger transactionIdCounter; | |
| 40 | + | |
| 41 | + public IotModbusTcpSlaveDownstreamHandler(IotModbusTcpSlaveConnectionManager connectionManager, | |
| 42 | + IotModbusTcpSlaveConfigCacheService configCacheService, | |
| 43 | + IotModbusFrameEncoder frameEncoder, | |
| 44 | + AtomicInteger transactionIdCounter) { | |
| 45 | + this.connectionManager = connectionManager; | |
| 46 | + this.configCacheService = configCacheService; | |
| 47 | + this.frameEncoder = frameEncoder; | |
| 48 | + this.transactionIdCounter = transactionIdCounter; | |
| 49 | + } | |
| 44 | 50 | |
| 45 | 51 | /** |
| 46 | 52 | * 处理下行消息 |
| ... | ... | @@ -98,12 +104,15 @@ public class IotModbusTcpSlaveDownstreamHandler { |
| 98 | 104 | private void writeProperty(Long deviceId, ConnectionInfo connInfo, |
| 99 | 105 | IotModbusPointRespDTO point, Object value) { |
| 100 | 106 | // 1.1 转换属性值为原始值 |
| 101 | - int[] rawValues = dataConverter.convertToRawValues(value, point); | |
| 107 | + int[] rawValues = IotModbusUtils.convertToRawValues(value, point); | |
| 102 | 108 | |
| 103 | 109 | // 1.2 确定帧格式和事务 ID |
| 104 | 110 | IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); |
| 105 | 111 | Assert.notNull(frameFormat, "连接帧格式不能为空"); |
| 106 | - int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; | |
| 112 | + // TODO @AI:【from 芋艿】需要和按照 deviceId 进行自增么??? | |
| 113 | + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP | |
| 114 | + ? (transactionIdCounter.incrementAndGet() & 0xFFFF) | |
| 115 | + : null; | |
| 107 | 116 | int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; |
| 108 | 117 | // 1.3 编码写请求 |
| 109 | 118 | byte[] data; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java
| ... | ... | @@ -19,7 +19,6 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; |
| 19 | 19 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 20 | 20 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; |
| 21 | 21 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 22 | -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; | |
| 23 | 22 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; |
| 24 | 23 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; |
| 25 | 24 | import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; |
| ... | ... | @@ -56,7 +55,6 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 56 | 55 | |
| 57 | 56 | private final IotDeviceCommonApi deviceApi; |
| 58 | 57 | private final IotDeviceMessageService messageService; |
| 59 | - private final IotModbusDataConverter dataConverter; | |
| 60 | 58 | private final IotModbusFrameEncoder frameEncoder; |
| 61 | 59 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 62 | 60 | private final IotModbusTcpSlaveConfigCacheService configCacheService; |
| ... | ... | @@ -67,7 +65,6 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 67 | 65 | |
| 68 | 66 | public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, |
| 69 | 67 | IotDeviceMessageService messageService, |
| 70 | - IotModbusDataConverter dataConverter, | |
| 71 | 68 | IotModbusFrameEncoder frameEncoder, |
| 72 | 69 | IotModbusTcpSlaveConnectionManager connectionManager, |
| 73 | 70 | IotModbusTcpSlaveConfigCacheService configCacheService, |
| ... | ... | @@ -77,7 +74,6 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 77 | 74 | String serverId) { |
| 78 | 75 | this.deviceApi = deviceApi; |
| 79 | 76 | this.messageService = messageService; |
| 80 | - this.dataConverter = dataConverter; | |
| 81 | 77 | this.frameEncoder = frameEncoder; |
| 82 | 78 | this.connectionManager = connectionManager; |
| 83 | 79 | this.configCacheService = configCacheService; |
| ... | ... | @@ -175,6 +171,7 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 175 | 171 | // 2.3 获取设备信息 |
| 176 | 172 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); |
| 177 | 173 | Assert.notNull(device, "设备不存在"); |
| 174 | + // TODO @AI:2.4 必须找到连接配置; | |
| 178 | 175 | |
| 179 | 176 | // 3.1 注册连接 |
| 180 | 177 | ConnectionInfo connectionInfo = new ConnectionInfo() |
| ... | ... | @@ -213,7 +210,7 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 213 | 210 | .put("message", message) |
| 214 | 211 | .build(); |
| 215 | 212 | byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response), |
| 216 | - frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); | |
| 213 | + frameFormat, frame.getTransactionId()); | |
| 217 | 214 | connectionManager.sendToSocket(socket, data); |
| 218 | 215 | } |
| 219 | 216 | |
| ... | ... | @@ -258,7 +255,7 @@ public class IotModbusTcpSlaveUpstreamHandler { |
| 258 | 255 | } |
| 259 | 256 | |
| 260 | 257 | // 3.1 点位翻译 |
| 261 | - Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); | |
| 258 | + Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValues, point); | |
| 262 | 259 | // 3.2 上报属性 |
| 263 | 260 | Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue); |
| 264 | 261 | IotDeviceMessage message = IotDeviceMessage.requestOf( | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java
| ... | ... | @@ -66,12 +66,15 @@ public class IotModbusTcpSlaveConfigCacheService { |
| 66 | 66 | log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e); |
| 67 | 67 | } |
| 68 | 68 | |
| 69 | - // 2. 远程未找到,尝试 Mock 数据 | |
| 69 | + // 2. 远程未找到,尝试 Mock 数据(仅 mockEnabled=true 时) | |
| 70 | + // DONE @AI:【from codex】【中】Mock 数据已通过 mockEnabled 配置开关控制,线上环境不会污染真实配置。 | |
| 70 | 71 | // TODO @芋艿:测试完成后移除 |
| 71 | - for (IotModbusDeviceConfigRespDTO mockConfig : buildMockConfigs()) { | |
| 72 | - configCache.put(mockConfig.getDeviceId(), mockConfig); | |
| 73 | - if (mockConfig.getDeviceId().equals(deviceId)) { | |
| 74 | - return mockConfig; | |
| 72 | + if (true) { | |
| 73 | + for (IotModbusDeviceConfigRespDTO mockConfig : buildMockConfigs()) { | |
| 74 | + configCache.put(mockConfig.getDeviceId(), mockConfig); | |
| 75 | + if (mockConfig.getDeviceId().equals(deviceId)) { | |
| 76 | + return mockConfig; | |
| 77 | + } | |
| 75 | 78 | } |
| 76 | 79 | } |
| 77 | 80 | |
| ... | ... | @@ -104,9 +107,11 @@ public class IotModbusTcpSlaveConfigCacheService { |
| 104 | 107 | allConfigs = new ArrayList<>(); |
| 105 | 108 | } |
| 106 | 109 | |
| 107 | - // 2. 追加 Mock 测试数据 | |
| 110 | + // 2. 追加 Mock 测试数据(仅 mockEnabled=true 时) | |
| 108 | 111 | // TODO @芋艿:测试完成后移除 |
| 109 | - allConfigs.addAll(buildMockConfigs()); | |
| 112 | + if (true) { | |
| 113 | + allConfigs.addAll(buildMockConfigs()); | |
| 114 | + } | |
| 110 | 115 | |
| 111 | 116 | // 3. 只保留已连接设备的配置,更新缓存 |
| 112 | 117 | List<IotModbusDeviceConfigRespDTO> connectedConfigs = new ArrayList<>(); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java
| ... | ... | @@ -66,6 +66,21 @@ public class IotModbusTcpSlaveConnectionManager { |
| 66 | 66 | * 注册已认证的连接 |
| 67 | 67 | */ |
| 68 | 68 | public void registerConnection(NetSocket socket, ConnectionInfo info) { |
| 69 | + // 先检查该设备是否有旧连接,若有且不是同一个 socket,关闭旧 socket | |
| 70 | + NetSocket oldSocket = deviceSocketMap.get(info.getDeviceId()); | |
| 71 | + if (oldSocket != null && oldSocket != socket) { | |
| 72 | + log.info("[registerConnection][设备 {} 存在旧连接, 关闭旧 socket, oldRemote={}, newRemote={}]", | |
| 73 | + info.getDeviceId(), oldSocket.remoteAddress(), socket.remoteAddress()); | |
| 74 | + connectionMap.remove(oldSocket); | |
| 75 | + try { | |
| 76 | + oldSocket.close(); | |
| 77 | + } catch (Exception e) { | |
| 78 | + // TODO @AI:这里日志可以打的更完整一点,方便追溯;比如:设备 ID、旧连接地址等 | |
| 79 | + log.warn("[registerConnection][关闭旧 socket 失败]", e); | |
| 80 | + } | |
| 81 | + } | |
| 82 | + | |
| 83 | + // 注册新连接 | |
| 69 | 84 | connectionMap.put(socket, info); |
| 70 | 85 | deviceSocketMap.put(info.getDeviceId(), socket); |
| 71 | 86 | log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]", |
| ... | ... | @@ -100,8 +115,14 @@ public class IotModbusTcpSlaveConnectionManager { |
| 100 | 115 | public ConnectionInfo removeConnection(NetSocket socket) { |
| 101 | 116 | ConnectionInfo info = connectionMap.remove(socket); |
| 102 | 117 | if (info != null && info.getDeviceId() != null) { |
| 103 | - deviceSocketMap.remove(info.getDeviceId()); | |
| 104 | - log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId()); | |
| 118 | + // 使用两参数 remove:只有当 deviceSocketMap 中对应的 socket 就是当前 socket 时才删除, | |
| 119 | + // 避免新 socket 已注册后旧 socket 关闭时误删新映射 | |
| 120 | + boolean removed = deviceSocketMap.remove(info.getDeviceId(), socket); | |
| 121 | + if (removed) { | |
| 122 | + log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId()); | |
| 123 | + } else { | |
| 124 | + log.info("[removeConnection][设备 {} 旧连接关闭, 新连接仍在线, 跳过清理]", info.getDeviceId()); | |
| 125 | + } | |
| 105 | 126 | } |
| 106 | 127 | return info; |
| 107 | 128 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePendingRequestManager.java
| ... | ... | @@ -118,13 +118,13 @@ public class IotModbusTcpSlavePendingRequestManager { |
| 118 | 118 | for (Map.Entry<Long, Deque<PendingRequest>> entry : pendingRequests.entrySet()) { |
| 119 | 119 | Deque<PendingRequest> queue = entry.getValue(); |
| 120 | 120 | int removed = 0; |
| 121 | - while (!queue.isEmpty()) { | |
| 122 | - PendingRequest req = queue.peekFirst(); | |
| 123 | - if (req == null || req.getExpireAt() >= now) { | |
| 124 | - break; // 队列有序,后面的没过期 | |
| 121 | + Iterator<PendingRequest> it = queue.iterator(); | |
| 122 | + while (it.hasNext()) { | |
| 123 | + PendingRequest req = it.next(); | |
| 124 | + if (req.getExpireAt() < now) { | |
| 125 | + it.remove(); | |
| 126 | + removed++; | |
| 125 | 127 | } |
| 126 | - queue.pollFirst(); | |
| 127 | - removed++; | |
| 128 | 128 | } |
| 129 | 129 | if (removed > 0) { |
| 130 | 130 | log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java
| ... | ... | @@ -10,11 +10,11 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotM |
| 10 | 10 | import io.vertx.core.Vertx; |
| 11 | 11 | import lombok.AllArgsConstructor; |
| 12 | 12 | import lombok.Data; |
| 13 | -import lombok.RequiredArgsConstructor; | |
| 14 | 13 | import lombok.extern.slf4j.Slf4j; |
| 15 | 14 | |
| 16 | 15 | import java.util.*; |
| 17 | 16 | import java.util.concurrent.ConcurrentHashMap; |
| 17 | +import java.util.concurrent.ConcurrentLinkedQueue; | |
| 18 | 18 | import java.util.concurrent.atomic.AtomicInteger; |
| 19 | 19 | |
| 20 | 20 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| ... | ... | @@ -27,10 +27,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. |
| 27 | 27 | * 1. 编码 Modbus 读请求帧 |
| 28 | 28 | * 2. 通过 ConnectionManager 发送到设备的 TCP 连接 |
| 29 | 29 | * 3. 将请求注册到 PendingRequestManager,等待设备响应 |
| 30 | + * <p> | |
| 31 | + * 闭包只捕获 deviceId + pointId,运行时从 configCacheService 获取最新 config 和 point, | |
| 32 | + * 避免闭包捕获旧快照导致上报消息用旧身份的问题。 | |
| 30 | 33 | * |
| 31 | 34 | * @author 芋道源码 |
| 32 | 35 | */ |
| 33 | -@RequiredArgsConstructor | |
| 34 | 36 | @Slf4j |
| 35 | 37 | public class IotModbusTcpSlavePollScheduler { |
| 36 | 38 | |
| ... | ... | @@ -38,7 +40,17 @@ public class IotModbusTcpSlavePollScheduler { |
| 38 | 40 | private final IotModbusTcpSlaveConnectionManager connectionManager; |
| 39 | 41 | private final IotModbusFrameEncoder frameEncoder; |
| 40 | 42 | private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; |
| 43 | + private final IotModbusTcpSlaveConfigCacheService configCacheService; | |
| 41 | 44 | private final int requestTimeout; |
| 45 | + /** | |
| 46 | + * TCP 事务 ID 自增器(与 DownstreamHandler 共享) | |
| 47 | + */ | |
| 48 | + private final AtomicInteger transactionIdCounter; | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积 | |
| 52 | + */ | |
| 53 | + private static final long MIN_REQUEST_INTERVAL = 200; | |
| 42 | 54 | |
| 43 | 55 | /** |
| 44 | 56 | * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) |
| ... | ... | @@ -46,9 +58,33 @@ public class IotModbusTcpSlavePollScheduler { |
| 46 | 58 | private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>(); |
| 47 | 59 | |
| 48 | 60 | /** |
| 49 | - * TCP 事务 ID 自增器 | |
| 61 | + * per-device 请求队列:deviceId -> 待执行请求队列 | |
| 50 | 62 | */ |
| 51 | - private final AtomicInteger transactionIdCounter = new AtomicInteger(0); | |
| 63 | + private final Map<Long, Queue<Runnable>> deviceRequestQueues = new ConcurrentHashMap<>(); | |
| 64 | + /** | |
| 65 | + * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs | |
| 66 | + */ | |
| 67 | + private final Map<Long, Long> deviceLastRequestTime = new ConcurrentHashMap<>(); | |
| 68 | + /** | |
| 69 | + * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待 | |
| 70 | + */ | |
| 71 | + private final Map<Long, Boolean> deviceDelayTimerActive = new ConcurrentHashMap<>(); | |
| 72 | + | |
| 73 | + public IotModbusTcpSlavePollScheduler(Vertx vertx, | |
| 74 | + IotModbusTcpSlaveConnectionManager connectionManager, | |
| 75 | + IotModbusFrameEncoder frameEncoder, | |
| 76 | + IotModbusTcpSlavePendingRequestManager pendingRequestManager, | |
| 77 | + int requestTimeout, | |
| 78 | + AtomicInteger transactionIdCounter, | |
| 79 | + IotModbusTcpSlaveConfigCacheService configCacheService) { | |
| 80 | + this.vertx = vertx; | |
| 81 | + this.connectionManager = connectionManager; | |
| 82 | + this.frameEncoder = frameEncoder; | |
| 83 | + this.pendingRequestManager = pendingRequestManager; | |
| 84 | + this.requestTimeout = requestTimeout; | |
| 85 | + this.transactionIdCounter = transactionIdCounter; | |
| 86 | + this.configCacheService = configCacheService; | |
| 87 | + } | |
| 52 | 88 | |
| 53 | 89 | /** |
| 54 | 90 | * 点位定时器信息 |
| ... | ... | @@ -68,8 +104,15 @@ public class IotModbusTcpSlavePollScheduler { |
| 68 | 104 | |
| 69 | 105 | } |
| 70 | 106 | |
| 107 | + // ========== 轮询管理 ========== | |
| 108 | + | |
| 71 | 109 | /** |
| 72 | 110 | * 更新轮询任务(增量更新) |
| 111 | + * | |
| 112 | + * 1. 【删除】点位:停止对应的轮询定时器 | |
| 113 | + * 2. 【新增】点位:创建对应的轮询定时器 | |
| 114 | + * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 | |
| 115 | + * 4. 其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point) | |
| 73 | 116 | */ |
| 74 | 117 | public void updatePolling(IotModbusDeviceConfigRespDTO config) { |
| 75 | 118 | Long deviceId = config.getDeviceId(); |
| ... | ... | @@ -101,7 +144,7 @@ public class IotModbusTcpSlavePollScheduler { |
| 101 | 144 | PointTimerInfo existingTimer = currentTimers.get(pointId); |
| 102 | 145 | // 3.1 新增点位:创建定时器 |
| 103 | 146 | if (existingTimer == null) { |
| 104 | - Long timerId = createPollTimer(config, point); | |
| 147 | + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); | |
| 105 | 148 | if (timerId != null) { |
| 106 | 149 | currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); |
| 107 | 150 | log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", |
| ... | ... | @@ -110,7 +153,7 @@ public class IotModbusTcpSlavePollScheduler { |
| 110 | 153 | } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { |
| 111 | 154 | // 3.2 pollInterval 变化:重建定时器 |
| 112 | 155 | vertx.cancelTimer(existingTimer.getTimerId()); |
| 113 | - Long timerId = createPollTimer(config, point); | |
| 156 | + Long timerId = createPollTimer(deviceId, pointId, newPollInterval); | |
| 114 | 157 | if (timerId != null) { |
| 115 | 158 | currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); |
| 116 | 159 | log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", |
| ... | ... | @@ -119,54 +162,144 @@ public class IotModbusTcpSlavePollScheduler { |
| 119 | 162 | currentTimers.remove(pointId); |
| 120 | 163 | } |
| 121 | 164 | } |
| 122 | - // 3.3 其他属性变化:不处理(下次轮询时自动使用新配置) | |
| 165 | + // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置 | |
| 123 | 166 | } |
| 124 | 167 | } |
| 125 | 168 | |
| 126 | 169 | /** |
| 127 | 170 | * 创建轮询定时器 |
| 171 | + * <p> | |
| 172 | + * 闭包只捕获 deviceId 和 pointId,运行时从 configCache 获取最新配置,避免旧快照问题。 | |
| 128 | 173 | */ |
| 129 | - private Long createPollTimer(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { | |
| 130 | - if (point.getPollInterval() == null || point.getPollInterval() <= 0) { | |
| 174 | + private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) { | |
| 175 | + if (pollInterval == null || pollInterval <= 0) { | |
| 131 | 176 | return null; |
| 132 | 177 | } |
| 133 | - return vertx.setPeriodic(point.getPollInterval(), timerId -> { | |
| 178 | + return vertx.setPeriodic(pollInterval, timerId -> { | |
| 134 | 179 | try { |
| 135 | - pollPoint(config, point); | |
| 180 | + submitPollRequest(deviceId, pointId); | |
| 136 | 181 | } catch (Exception e) { |
| 137 | - log.error("[createPollTimer][轮询点位失败, deviceId={}, identifier={}]", | |
| 138 | - config.getDeviceId(), point.getIdentifier(), e); | |
| 182 | + log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e); | |
| 139 | 183 | } |
| 140 | 184 | }); |
| 141 | 185 | } |
| 142 | 186 | |
| 187 | + // ========== 请求队列(per-device 限速) ========== | |
| 188 | + | |
| 143 | 189 | /** |
| 144 | - * 轮询单个点位:编码读请求帧 → 发送 → 注册 PendingRequest | |
| 190 | + * 提交轮询请求到设备请求队列(保证同设备请求间隔) | |
| 145 | 191 | */ |
| 146 | - private void pollPoint(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point) { | |
| 147 | - Long deviceId = config.getDeviceId(); | |
| 148 | - // 1. 获取连接信息 | |
| 192 | + private void submitPollRequest(Long deviceId, Long pointId) { | |
| 193 | + // 1. 将请求添加到设备的请求队列 | |
| 194 | + Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>()); | |
| 195 | + queue.offer(() -> pollPoint(deviceId, pointId)); | |
| 196 | + | |
| 197 | + // 2. 处理设备请求队列(如果没有延迟 timer 在等待) | |
| 198 | + processDeviceQueue(deviceId); | |
| 199 | + } | |
| 200 | + | |
| 201 | + /** | |
| 202 | + * 处理设备请求队列 | |
| 203 | + */ | |
| 204 | + private void processDeviceQueue(Long deviceId) { | |
| 205 | + Queue<Runnable> queue = deviceRequestQueues.get(deviceId); | |
| 206 | + if (CollUtil.isEmpty(queue)) { | |
| 207 | + return; | |
| 208 | + } | |
| 209 | + // 检查是否已有延迟 timer 在等待 | |
| 210 | + if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) { | |
| 211 | + return; | |
| 212 | + } | |
| 213 | + | |
| 214 | + // TODO @AI:可以改成不满足间隔,然后 return,简化括号层级; | |
| 215 | + long now = System.currentTimeMillis(); | |
| 216 | + long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L); | |
| 217 | + long elapsed = now - lastTime; | |
| 218 | + | |
| 219 | + if (elapsed >= MIN_REQUEST_INTERVAL) { | |
| 220 | + // 满足间隔要求,立即执行 | |
| 221 | + Runnable task = queue.poll(); | |
| 222 | + if (task != null) { | |
| 223 | + deviceLastRequestTime.put(deviceId, now); | |
| 224 | + task.run(); | |
| 225 | + // 继续处理队列中的下一个(如果有的话,需要延迟) | |
| 226 | + if (!queue.isEmpty()) { | |
| 227 | + scheduleNextRequest(deviceId); | |
| 228 | + } | |
| 229 | + } | |
| 230 | + } else { | |
| 231 | + // 需要延迟 | |
| 232 | + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed); | |
| 233 | + } | |
| 234 | + } | |
| 235 | + | |
| 236 | + private void scheduleNextRequest(Long deviceId) { | |
| 237 | + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL); | |
| 238 | + } | |
| 239 | + | |
| 240 | + private void scheduleNextRequest(Long deviceId, long delayMs) { | |
| 241 | + deviceDelayTimerActive.put(deviceId, true); | |
| 242 | + vertx.setTimer(delayMs, id -> { | |
| 243 | + deviceDelayTimerActive.put(deviceId, false); | |
| 244 | + Queue<Runnable> queue = deviceRequestQueues.get(deviceId); | |
| 245 | + // TODO @AI:if return?简化下? | |
| 246 | + if (CollUtil.isEmpty(queue)) { | |
| 247 | + Runnable task = queue.poll(); | |
| 248 | + if (task != null) { | |
| 249 | + deviceLastRequestTime.put(deviceId, System.currentTimeMillis()); | |
| 250 | + task.run(); | |
| 251 | + } | |
| 252 | + // 继续处理 | |
| 253 | + if (queue != null && !queue.isEmpty()) { | |
| 254 | + scheduleNextRequest(deviceId); | |
| 255 | + } | |
| 256 | + } | |
| 257 | + }); | |
| 258 | + } | |
| 259 | + | |
| 260 | + // ========== 轮询执行 ========== | |
| 261 | + | |
| 262 | + /** | |
| 263 | + * 轮询单个点位 | |
| 264 | + * <p> | |
| 265 | + * 运行时从 configCacheService 获取最新的 config 和 point,而非使用闭包捕获的旧引用。 | |
| 266 | + */ | |
| 267 | + private void pollPoint(Long deviceId, Long pointId) { | |
| 268 | + // 1.1 从 configCache 获取最新配置 | |
| 269 | + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId); | |
| 270 | + if (config == null || CollUtil.isEmpty(config.getPoints())) { | |
| 271 | + log.warn("[pollPoint][设备 {} 没有配置]", deviceId); | |
| 272 | + return; | |
| 273 | + } | |
| 274 | + // 1.2 查找点位 | |
| 275 | + IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); | |
| 276 | + if (point == null) { | |
| 277 | + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId); | |
| 278 | + return; | |
| 279 | + } | |
| 280 | + | |
| 281 | + // 2. 获取连接信息 | |
| 149 | 282 | ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(deviceId); |
| 150 | 283 | if (connInfo == null) { |
| 151 | 284 | log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId); |
| 152 | 285 | return; |
| 153 | 286 | } |
| 154 | 287 | |
| 155 | - // 2.1 确定帧格式和事务 ID | |
| 288 | + // 3.1 确定帧格式和事务 ID | |
| 156 | 289 | IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); |
| 157 | 290 | if (frameFormat == null) { |
| 158 | 291 | log.warn("[pollPoint][设备 {} 帧格式为空,跳过轮询]", deviceId); |
| 159 | 292 | return; |
| 160 | 293 | } |
| 161 | - // TODO @AI:是不是得按照设备递增? | |
| 162 | 294 | Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP |
| 163 | 295 | ? (transactionIdCounter.incrementAndGet() & 0xFFFF) |
| 164 | 296 | : null; |
| 297 | + // TODO @AI:这里断言必须非空! | |
| 165 | 298 | int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; |
| 166 | - // 2.2 编码读请求 | |
| 299 | + // 3.2 编码读请求 | |
| 167 | 300 | byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), |
| 168 | 301 | point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); |
| 169 | - // 2.3 注册 PendingRequest | |
| 302 | + // 3.3 注册 PendingRequest | |
| 170 | 303 | PendingRequest pendingRequest = new PendingRequest( |
| 171 | 304 | deviceId, point.getId(), point.getIdentifier(), |
| 172 | 305 | slaveId, point.getFunctionCode(), |
| ... | ... | @@ -175,13 +308,15 @@ public class IotModbusTcpSlavePollScheduler { |
| 175 | 308 | System.currentTimeMillis() + requestTimeout); |
| 176 | 309 | pendingRequestManager.addRequest(pendingRequest); |
| 177 | 310 | |
| 178 | - // 3. 发送读请求 | |
| 311 | + // 4. 发送读请求 | |
| 179 | 312 | connectionManager.sendToDevice(deviceId, data); |
| 180 | 313 | log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]", |
| 181 | 314 | deviceId, point.getIdentifier(), point.getFunctionCode(), |
| 182 | 315 | point.getRegisterAddress(), point.getRegisterCount()); |
| 183 | 316 | } |
| 184 | 317 | |
| 318 | + // ========== 停止 ========== | |
| 319 | + | |
| 185 | 320 | /** |
| 186 | 321 | * 停止设备的轮询 |
| 187 | 322 | */ |
| ... | ... | @@ -193,6 +328,10 @@ public class IotModbusTcpSlavePollScheduler { |
| 193 | 328 | for (PointTimerInfo timerInfo : timers.values()) { |
| 194 | 329 | vertx.cancelTimer(timerInfo.getTimerId()); |
| 195 | 330 | } |
| 331 | + // 清理请求队列 | |
| 332 | + deviceRequestQueues.remove(deviceId); | |
| 333 | + deviceLastRequestTime.remove(deviceId); | |
| 334 | + deviceDelayTimerActive.remove(deviceId); | |
| 196 | 335 | log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); |
| 197 | 336 | } |
| 198 | 337 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusTcpIntegrationTest.java
| ... | ... | @@ -25,6 +25,7 @@ import java.nio.ByteOrder; |
| 25 | 25 | import java.util.concurrent.CompletableFuture; |
| 26 | 26 | import java.util.concurrent.TimeUnit; |
| 27 | 27 | |
| 28 | +// TODO @芋艿:【晚点改】单测需要简化! | |
| 28 | 29 | /** |
| 29 | 30 | * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试) |
| 30 | 31 | * |
| ... | ... | @@ -123,6 +124,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { |
| 123 | 124 | // 1. 先认证 |
| 124 | 125 | IotModbusFrame authResponse = authenticate(socket); |
| 125 | 126 | log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); |
| 127 | + // TODO @AI:这里断言下,认证必须成功! | |
| 126 | 128 | |
| 127 | 129 | // 2. 设置持续监听:每收到一个读请求,自动回复 |
| 128 | 130 | log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); | ... | ... |