Commit e13cd545cced2a8f7ae11e3f80aeca5c428fcc08

Authored by YunaiV
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 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common;
2 2
3 import cn.hutool.core.collection.CollUtil; 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 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; 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 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
  12 +import lombok.experimental.UtilityClass;
7 import lombok.extern.slf4j.Slf4j; 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 * <p> 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 * @author 芋道源码 33 * @author 芋道源码
19 */ 34 */
  35 +@UtilityClass
20 @Slf4j 36 @Slf4j
21 public class IotModbusUtils { 37 public class IotModbusUtils {
22 38
@@ -139,19 +155,6 @@ public class IotModbusUtils { @@ -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 // ==================== CRC16 工具 ==================== 158 // ==================== CRC16 工具 ====================
156 159
157 /** 160 /**
@@ -192,7 +195,253 @@ public class IotModbusUtils { @@ -192,7 +195,253 @@ public class IotModbusUtils {
192 return computed == received; 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 * 从帧中提取寄存器值(FC01-04 读响应) 447 * 从帧中提取寄存器值(FC01-04 读响应)
@@ -224,10 +473,6 @@ public class IotModbusUtils { @@ -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 private static int[] extractCoilValues(byte[] pdu) { 476 private static int[] extractCoilValues(byte[] pdu) {
232 if (pdu.length < 2) { 477 if (pdu.length < 2) {
233 return null; 478 return null;
@@ -241,10 +486,6 @@ public class IotModbusUtils { @@ -241,10 +486,6 @@ public class IotModbusUtils {
241 return values; 486 return values;
242 } 487 }
243 488
244 - /**  
245 - * 提取寄存器值  
246 - * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)]  
247 - */  
248 private static int[] extractRegisterValues(byte[] pdu) { 489 private static int[] extractRegisterValues(byte[] pdu) {
249 if (pdu.length < 2) { 490 if (pdu.length < 2) {
250 return null; 491 return null;
@@ -258,4 +499,17 @@ public class IotModbusUtils { @@ -258,4 +499,17 @@ public class IotModbusUtils {
258 return values; 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,11 +6,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
7 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; 7 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
8 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 8 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
10 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; 11 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; 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 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamHandler; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamHandler;
15 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamSubscriber; 14 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream.IotModbusTcpDownstreamSubscriber;
16 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; 15 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler;
@@ -70,6 +69,7 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { @@ -70,6 +69,7 @@ public class IotModbusTcpMasterProtocol implements IotProtocol {
70 69
71 private final IotModbusTcpConfigCacheService configCacheService; 70 private final IotModbusTcpConfigCacheService configCacheService;
72 private final IotModbusTcpPollScheduler pollScheduler; 71 private final IotModbusTcpPollScheduler pollScheduler;
  72 + private final IotDeviceMessageService messageService;
73 73
74 public IotModbusTcpMasterProtocol(ProtocolProperties properties) { 74 public IotModbusTcpMasterProtocol(ProtocolProperties properties) {
75 IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster(); 75 IotModbusTcpMasterConfig modbusTcpMasterConfig = properties.getModbusTcpMaster();
@@ -87,15 +87,14 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { @@ -87,15 +87,14 @@ public class IotModbusTcpMasterProtocol implements IotProtocol {
87 this.configCacheService = new IotModbusTcpConfigCacheService(deviceApi); 87 this.configCacheService = new IotModbusTcpConfigCacheService(deviceApi);
88 88
89 // 初始化 Handler 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 IotModbusTcpDownstreamHandler downstreamHandler = new IotModbusTcpDownstreamHandler(connectionManager, 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 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); 100 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
@@ -186,24 +185,54 @@ public class IotModbusTcpMasterProtocol implements IotProtocol { @@ -186,24 +185,54 @@ public class IotModbusTcpMasterProtocol implements IotProtocol {
186 */ 185 */
187 private synchronized void refreshConfig() { 186 private synchronized void refreshConfig() {
188 try { 187 try {
189 - // 1. 从 biz 拉取最新配置 188 + // 1. 从 biz 拉取最新配置(API 失败时返回 null)
190 List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig(); 189 List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig();
  190 + if (configs == null) {
  191 + log.warn("[refreshConfig][API 失败,跳过本轮刷新]");
  192 + return;
  193 + }
191 log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size()); 194 log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size());
192 195
193 // 2. 更新连接和轮询任务 196 // 2. 更新连接和轮询任务
194 for (IotModbusDeviceConfigRespDTO config : configs) { 197 for (IotModbusDeviceConfigRespDTO config : configs) {
195 try { 198 try {
196 - // 2.1 确保连接存在 199 + // 2.1 检测是否为首次连接
  200 + boolean isNewConnection = connectionManager.getConnection(config.getDeviceId()) == null;
  201 + // 2.2 确保连接存在
197 connectionManager.ensureConnection(config); 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 pollScheduler.updatePolling(config); 215 pollScheduler.updatePolling(config);
200 } catch (Exception e) { 216 } catch (Exception e) {
201 log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e); 217 log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
202 } 218 }
203 } 219 }
204 220
205 - // 3. 清理已删除设备的资源 221 + // 3. 清理已删除设备的资源(仅 API 成功时才执行)
206 configCacheService.cleanupRemovedDevices(configs, deviceId -> { 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 pollScheduler.stopPolling(deviceId); 236 pollScheduler.stopPolling(deviceId);
208 connectionManager.removeDevice(deviceId); 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,22 +9,24 @@ import com.ghgande.j2mod.modbus.procimg.Register;
9 import com.ghgande.j2mod.modbus.procimg.SimpleRegister; 9 import com.ghgande.j2mod.modbus.procimg.SimpleRegister;
10 import com.ghgande.j2mod.modbus.util.BitVector; 10 import com.ghgande.j2mod.modbus.util.BitVector;
11 import io.vertx.core.Future; 11 import io.vertx.core.Future;
  12 +import lombok.experimental.UtilityClass;
12 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
13 14
14 import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils.*; 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 * <p> 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 * @author 芋道源码 25 * @author 芋道源码
25 */ 26 */
  27 +@UtilityClass
26 @Slf4j 28 @Slf4j
27 -public class IotModbusTcpClient { 29 +public class IotModbusTcpClientUtils {
28 30
29 /** 31 /**
30 * 读取 Modbus 数据 32 * 读取 Modbus 数据
@@ -34,7 +36,7 @@ public class IotModbusTcpClient { @@ -34,7 +36,7 @@ public class IotModbusTcpClient {
34 * @param point 点位配置 36 * @param point 点位配置
35 * @return 原始值(int 数组) 37 * @return 原始值(int 数组)
36 */ 38 */
37 - public Future<int[]> read(IotModbusTcpConnectionManager.ModbusConnection connection, 39 + public static Future<int[]> read(IotModbusTcpConnectionManager.ModbusConnection connection,
38 Integer slaveId, 40 Integer slaveId,
39 IotModbusPointRespDTO point) { 41 IotModbusPointRespDTO point) {
40 return connection.executeBlocking(tcpConnection -> { 42 return connection.executeBlocking(tcpConnection -> {
@@ -68,7 +70,7 @@ public class IotModbusTcpClient { @@ -68,7 +70,7 @@ public class IotModbusTcpClient {
68 * @param values 要写入的值 70 * @param values 要写入的值
69 * @return 是否成功 71 * @return 是否成功
70 */ 72 */
71 - public Future<Boolean> write(IotModbusTcpConnectionManager.ModbusConnection connection, 73 + public static Future<Boolean> write(IotModbusTcpConnectionManager.ModbusConnection connection,
72 Integer slaveId, 74 Integer slaveId,
73 IotModbusPointRespDTO point, 75 IotModbusPointRespDTO point,
74 int[] values) { 76 int[] values) {
@@ -98,7 +100,7 @@ public class IotModbusTcpClient { @@ -98,7 +100,7 @@ public class IotModbusTcpClient {
98 * 创建读取请求 100 * 创建读取请求
99 */ 101 */
100 @SuppressWarnings("EnhancedSwitchMigration") 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 switch (functionCode) { 104 switch (functionCode) {
103 case FC_READ_COILS: 105 case FC_READ_COILS:
104 return new ReadCoilsRequest(address, count); 106 return new ReadCoilsRequest(address, count);
@@ -117,7 +119,7 @@ public class IotModbusTcpClient { @@ -117,7 +119,7 @@ public class IotModbusTcpClient {
117 * 创建写入请求 119 * 创建写入请求
118 */ 120 */
119 @SuppressWarnings("EnhancedSwitchMigration") 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 switch (functionCode) { 123 switch (functionCode) {
122 case FC_READ_COILS: // 写线圈(使用功能码 5 或 15) 124 case FC_READ_COILS: // 写线圈(使用功能码 5 或 15)
123 if (count == 1) { 125 if (count == 1) {
@@ -151,7 +153,7 @@ public class IotModbusTcpClient { @@ -151,7 +153,7 @@ public class IotModbusTcpClient {
151 * 从响应中提取值 153 * 从响应中提取值
152 */ 154 */
153 @SuppressWarnings("EnhancedSwitchMigration") 155 @SuppressWarnings("EnhancedSwitchMigration")
154 - private int[] extractValues(ModbusResponse response, Integer functionCode) { 156 + private static int[] extractValues(ModbusResponse response, Integer functionCode) {
155 switch (functionCode) { 157 switch (functionCode) {
156 case FC_READ_COILS: 158 case FC_READ_COILS:
157 ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; 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,9 +4,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;  
8 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; 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 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
12 import lombok.RequiredArgsConstructor; 11 import lombok.RequiredArgsConstructor;
@@ -28,8 +27,6 @@ import java.util.Map; @@ -28,8 +27,6 @@ import java.util.Map;
28 public class IotModbusTcpDownstreamHandler { 27 public class IotModbusTcpDownstreamHandler {
29 28
30 private final IotModbusTcpConnectionManager connectionManager; 29 private final IotModbusTcpConnectionManager connectionManager;
31 - private final IotModbusTcpClient modbusClient;  
32 - private final IotModbusDataConverter dataConverter;  
33 private final IotModbusTcpConfigCacheService configCacheService; 30 private final IotModbusTcpConfigCacheService configCacheService;
34 31
35 /** 32 /**
@@ -94,9 +91,9 @@ public class IotModbusTcpDownstreamHandler { @@ -94,9 +91,9 @@ public class IotModbusTcpDownstreamHandler {
94 } 91 }
95 92
96 // 2.1 转换属性值为原始值 93 // 2.1 转换属性值为原始值
97 - int[] rawValues = dataConverter.convertToRawValues(value, point); 94 + int[] rawValues = IotModbusUtils.convertToRawValues(value, point);
98 // 2.2 执行 Modbus 写入 95 // 2.2 执行 Modbus 写入
99 - modbusClient.write(connection, slaveId, point, rawValues) 96 + IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues)
100 .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", 97 .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
101 config.getDeviceId(), point.getIdentifier(), value)) 98 config.getDeviceId(), point.getIdentifier(), value))
102 .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]", 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,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 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 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
10 import lombok.extern.slf4j.Slf4j; 10 import lombok.extern.slf4j.Slf4j;
11 11
@@ -20,14 +20,11 @@ import java.util.Map; @@ -20,14 +20,11 @@ import java.util.Map;
20 public class IotModbusTcpUpstreamHandler { 20 public class IotModbusTcpUpstreamHandler {
21 21
22 private final IotDeviceMessageService messageService; 22 private final IotDeviceMessageService messageService;
23 - private final IotModbusDataConverter dataConverter;  
24 private final String serverId; 23 private final String serverId;
25 24
26 public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService, 25 public IotModbusTcpUpstreamHandler(IotDeviceMessageService messageService,
27 - IotModbusDataConverter dataConverter,  
28 String serverId) { 26 String serverId) {
29 this.messageService = messageService; 27 this.messageService = messageService;
30 - this.dataConverter = dataConverter;  
31 this.serverId = serverId; 28 this.serverId = serverId;
32 } 29 }
33 30
@@ -43,7 +40,7 @@ public class IotModbusTcpUpstreamHandler { @@ -43,7 +40,7 @@ public class IotModbusTcpUpstreamHandler {
43 int[] rawValue) { 40 int[] rawValue) {
44 try { 41 try {
45 // 1.1 转换原始值为物模型属性值 42 // 1.1 转换原始值为物模型属性值
46 - Object convertedValue = dataConverter.convertToPropertyValue(rawValue, point); 43 + Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValue, point);
47 log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]", 44 log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]",
48 config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue); 45 config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue);
49 // 1.2 构造属性上报消息 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,16 +38,13 @@ public class IotModbusTcpConfigCacheService {
38 /** 38 /**
39 * 刷新配置 39 * 刷新配置
40 * 40 *
41 - * @return 最新的配置列表 41 + * @return 最新的配置列表;API 失败时返回 null(调用方应跳过 cleanup)
42 */ 42 */
43 public List<IotModbusDeviceConfigRespDTO> refreshConfig() { 43 public List<IotModbusDeviceConfigRespDTO> refreshConfig() {
44 try { 44 try {
45 // 1. 从远程获取配置 45 // 1. 从远程获取配置
46 CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getEnabledModbusDeviceConfigs(); 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 List<IotModbusDeviceConfigRespDTO> configs = result.getData(); 48 List<IotModbusDeviceConfigRespDTO> configs = result.getData();
52 49
53 // 2. 更新缓存(注意:不在这里更新 knownDeviceIds,由 cleanupRemovedDevices 统一管理) 50 // 2. 更新缓存(注意:不在这里更新 knownDeviceIds,由 cleanupRemovedDevices 统一管理)
@@ -57,7 +54,7 @@ public class IotModbusTcpConfigCacheService { @@ -57,7 +54,7 @@ public class IotModbusTcpConfigCacheService {
57 return configs; 54 return configs;
58 } catch (Exception e) { 55 } catch (Exception e) {
59 log.error("[refreshConfig][刷新配置失败]", e); 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,11 +81,11 @@ public class IotModbusTcpConfigCacheService {
84 Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds); 81 Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds);
85 removedDeviceIds.removeAll(currentDeviceIds); 82 removedDeviceIds.removeAll(currentDeviceIds);
86 83
87 - // 2. 清理已删除设备 84 + // 2. 清理已删除设备(先执行 cleanupAction,再从缓存移除,保证 action 中仍可获取 config)
88 for (Long deviceId : removedDeviceIds) { 85 for (Long deviceId : removedDeviceIds) {
89 log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId); 86 log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId);
90 - configCache.remove(deviceId);  
91 cleanupAction.accept(deviceId); 87 cleanupAction.accept(deviceId);
  88 + configCache.remove(deviceId);
92 } 89 }
93 90
94 // 3. 更新已知设备 ID 集合为当前有效的设备 ID 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 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
2 2
  3 +import cn.hutool.core.util.ObjUtil;
3 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
4 import com.ghgande.j2mod.modbus.net.TCPMasterConnection; 5 import com.ghgande.j2mod.modbus.net.TCPMasterConnection;
5 import io.vertx.core.Context; 6 import io.vertx.core.Context;
@@ -50,8 +51,15 @@ public class IotModbusTcpConnectionManager { @@ -50,8 +51,15 @@ public class IotModbusTcpConnectionManager {
50 * @param config 设备配置 51 * @param config 设备配置
51 */ 52 */
52 public void ensureConnection(IotModbusDeviceConfigRespDTO config) { 53 public void ensureConnection(IotModbusDeviceConfigRespDTO config) {
53 - // 1. 记录设备与连接的关系 54 + // 1.1 检查设备是否切换了 IP/端口,若是则先清理旧连接
54 String connectionKey = buildConnectionKey(config.getIp(), config.getPort()); 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 deviceConnectionMap.put(config.getDeviceId(), connectionKey); 63 deviceConnectionMap.put(config.getDeviceId(), connectionKey);
56 64
57 // 2. 情况一:连接已存在,添加设备引用 65 // 2. 情况一:连接已存在,添加设备引用
@@ -68,8 +76,15 @@ public class IotModbusTcpConnectionManager { @@ -68,8 +76,15 @@ public class IotModbusTcpConnectionManager {
68 log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey); 76 log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey);
69 return; 77 return;
70 } 78 }
71 - // 3.2 创建新连接 79 + // 3.2 double-check:拿到锁后再次检查,避免并发创建重复连接
72 try { 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 connection = createConnection(config, lock); 88 connection = createConnection(config, lock);
74 connectionPool.put(connectionKey, connection); 89 connectionPool.put(connectionKey, connection);
75 log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey); 90 log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey);
@@ -154,10 +169,10 @@ public class IotModbusTcpConnectionManager { @@ -154,10 +169,10 @@ public class IotModbusTcpConnectionManager {
154 if (connection.getTcpConnection() != null) { 169 if (connection.getTcpConnection() != null) {
155 connection.getTcpConnection().close(); 170 connection.getTcpConnection().close();
156 } 171 }
157 - // 安全释放锁:先检查锁存在且被锁定,再检查是否当前线程持有 172 + // 强制解锁,避免死锁(正常情况下应该不会发生锁未释放的情况)
158 RLock lock = connection.getLock(); 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 log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey); 177 log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey);
163 } catch (Exception e) { 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,33 +3,37 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6 -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; 6 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClientUtils;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler;
8 import io.vertx.core.Vertx; 8 import io.vertx.core.Vertx;
9 import lombok.AllArgsConstructor; 9 import lombok.AllArgsConstructor;
10 import lombok.Data; 10 import lombok.Data;
11 -import lombok.RequiredArgsConstructor;  
12 import lombok.extern.slf4j.Slf4j; 11 import lombok.extern.slf4j.Slf4j;
13 12
14 import java.util.*; 13 import java.util.*;
15 import java.util.concurrent.ConcurrentHashMap; 14 import java.util.concurrent.ConcurrentHashMap;
  15 +import java.util.concurrent.ConcurrentLinkedQueue;
16 16
17 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; 17 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
18 18
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 也要! 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 * @author 芋道源码 23 * @author 芋道源码
24 */ 24 */
25 -@RequiredArgsConstructor  
26 @Slf4j 25 @Slf4j
27 public class IotModbusTcpPollScheduler { 26 public class IotModbusTcpPollScheduler {
28 27
29 private final Vertx vertx; 28 private final Vertx vertx;
30 private final IotModbusTcpConnectionManager connectionManager; 29 private final IotModbusTcpConnectionManager connectionManager;
31 - private final IotModbusTcpClient modbusClient;  
32 private final IotModbusTcpUpstreamHandler upstreamHandler; 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 * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) 39 * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
@@ -37,6 +41,33 @@ public class IotModbusTcpPollScheduler { @@ -37,6 +41,33 @@ public class IotModbusTcpPollScheduler {
37 private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>(); 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 @Data 73 @Data
@@ -54,20 +85,22 @@ public class IotModbusTcpPollScheduler { @@ -54,20 +85,22 @@ public class IotModbusTcpPollScheduler {
54 85
55 } 86 }
56 87
  88 + // ========== 轮询管理 ==========
  89 +
57 /** 90 /**
58 * 更新轮询任务(增量更新) 91 * 更新轮询任务(增量更新)
59 * 92 *
60 * 1. 【删除】点位:停止对应的轮询定时器 93 * 1. 【删除】点位:停止对应的轮询定时器
61 * 2. 【新增】点位:创建对应的轮询定时器 94 * 2. 【新增】点位:创建对应的轮询定时器
62 * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器 95 * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器
63 - * 4. 其他属性变化(包括未变化的):不处理(下次轮询时自动使用新配置) 96 + * 4. 其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point)
64 */ 97 */
65 public void updatePolling(IotModbusDeviceConfigRespDTO config) { 98 public void updatePolling(IotModbusDeviceConfigRespDTO config) {
66 Long deviceId = config.getDeviceId(); 99 Long deviceId = config.getDeviceId();
67 List<IotModbusPointRespDTO> newPoints = config.getPoints(); 100 List<IotModbusPointRespDTO> newPoints = config.getPoints();
68 Map<Long, PointTimerInfo> currentTimers = devicePointTimers 101 Map<Long, PointTimerInfo> currentTimers = devicePointTimers
69 .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>()); 102 .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
70 - // 1.1 计算新配置(包括新增和修改的点位)中的点位 ID 集合 103 + // 1.1 计算新配置中的点位 ID 集合
71 Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId); 104 Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
72 // 1.2 计算删除的点位 ID 集合 105 // 1.2 计算删除的点位 ID 集合
73 Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet()); 106 Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
@@ -92,7 +125,7 @@ public class IotModbusTcpPollScheduler { @@ -92,7 +125,7 @@ public class IotModbusTcpPollScheduler {
92 PointTimerInfo existingTimer = currentTimers.get(pointId); 125 PointTimerInfo existingTimer = currentTimers.get(pointId);
93 // 3.1 新增点位:创建定时器 126 // 3.1 新增点位:创建定时器
94 if (existingTimer == null) { 127 if (existingTimer == null) {
95 - Long timerId = createPollTimer(config, point); 128 + Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
96 if (timerId != null) { 129 if (timerId != null) {
97 currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); 130 currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
98 log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", 131 log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
@@ -101,7 +134,7 @@ public class IotModbusTcpPollScheduler { @@ -101,7 +134,7 @@ public class IotModbusTcpPollScheduler {
101 } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { 134 } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
102 // 3.2 pollInterval 变化:重建定时器 135 // 3.2 pollInterval 变化:重建定时器
103 vertx.cancelTimer(existingTimer.getTimerId()); 136 vertx.cancelTimer(existingTimer.getTimerId());
104 - Long timerId = createPollTimer(config, point); 137 + Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
105 if (timerId != null) { 138 if (timerId != null) {
106 currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); 139 currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
107 log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", 140 log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
@@ -110,51 +143,136 @@ public class IotModbusTcpPollScheduler { @@ -110,51 +143,136 @@ public class IotModbusTcpPollScheduler {
110 currentTimers.remove(pointId); 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 return null; 157 return null;
123 } 158 }
124 - return vertx.setPeriodic(point.getPollInterval(), timerId -> { 159 + return vertx.setPeriodic(pollInterval, timerId -> {
125 try { 160 try {
126 - pollPoint(config, point); 161 + submitPollRequest(deviceId, pointId);
127 } catch (Exception e) { 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 if (connection == null) { 256 if (connection == null) {
141 - log.warn("[pollPoint][设备 {} 没有连接]", config.getDeviceId()); 257 + log.warn("[pollPoint][设备 {} 没有连接]", deviceId);
142 return; 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 if (slaveId == null) { 262 if (slaveId == null) {
147 - log.warn("[pollPoint][设备 {} 没有 slaveId]", config.getDeviceId()); 263 + log.warn("[pollPoint][设备 {} 没有 slaveId]", deviceId);
148 return; 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 .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue)) 269 .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue))
154 .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]", 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,6 +284,10 @@ public class IotModbusTcpPollScheduler {
166 for (PointTimerInfo timerInfo : timers.values()) { 284 for (PointTimerInfo timerInfo : timers.values()) {
167 vertx.cancelTimer(timerInfo.getTimerId()); 285 vertx.cancelTimer(timerInfo.getTimerId());
168 } 286 }
  287 + // 清理请求队列
  288 + deviceRequestQueues.remove(deviceId);
  289 + deviceLastRequestTime.remove(deviceId);
  290 + deviceDelayTimerActive.remove(deviceId);
169 log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); 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,4 +41,10 @@ public class IotModbusTcpSlaveConfig {
41 @NotNull(message = "请求清理间隔不能为空") 41 @NotNull(message = "请求清理间隔不能为空")
42 private Integer requestCleanupInterval = 10000; 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,10 +7,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
8 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; 8 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
9 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 9 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  10 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
11 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties; 12 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
12 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
13 -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;  
14 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; 14 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder;
15 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; 15 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
16 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler; 16 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream.IotModbusTcpSlaveDownstreamHandler;
@@ -34,6 +34,7 @@ import lombok.extern.slf4j.Slf4j; @@ -34,6 +34,7 @@ import lombok.extern.slf4j.Slf4j;
34 import java.util.List; 34 import java.util.List;
35 import java.util.Set; 35 import java.util.Set;
36 import java.util.concurrent.TimeUnit; 36 import java.util.concurrent.TimeUnit;
  37 +import java.util.concurrent.atomic.AtomicInteger;
37 38
38 /** 39 /**
39 * IoT 网关 Modbus TCP Slave 协议 40 * IoT 网关 Modbus TCP Slave 协议
@@ -92,6 +93,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { @@ -92,6 +93,7 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
92 private final IotModbusTcpSlaveUpstreamHandler upstreamHandler; 93 private final IotModbusTcpSlaveUpstreamHandler upstreamHandler;
93 private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber; 94 private final IotModbusTcpSlaveDownstreamSubscriber downstreamSubscriber;
94 private final IotModbusTcpSlavePollScheduler pollScheduler; 95 private final IotModbusTcpSlavePollScheduler pollScheduler;
  96 + private final IotDeviceMessageService messageService;
95 97
96 public IotModbusTcpSlaveProtocol(ProtocolProperties properties) { 98 public IotModbusTcpSlaveProtocol(ProtocolProperties properties) {
97 this.slaveConfig = properties.getModbusTcpSlave(); 99 this.slaveConfig = properties.getModbusTcpSlave();
@@ -112,24 +114,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { @@ -112,24 +114,27 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
112 this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode()); 114 this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode());
113 this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode()); 115 this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode());
114 116
  117 + // 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突)
  118 + AtomicInteger transactionIdCounter = new AtomicInteger(0);
  119 +
115 // 初始化轮询调度器 120 // 初始化轮询调度器
116 this.pollScheduler = new IotModbusTcpSlavePollScheduler( 121 this.pollScheduler = new IotModbusTcpSlavePollScheduler(
117 vertx, connectionManager, frameEncoder, pendingRequestManager, 122 vertx, connectionManager, frameEncoder, pendingRequestManager,
118 - slaveConfig.getRequestTimeout()); 123 + slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService);
119 124
120 // 初始化 Handler 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 IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class); 128 IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
124 this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler( 129 this.upstreamHandler = new IotModbusTcpSlaveUpstreamHandler(
125 - deviceApi, messageService, dataConverter, frameEncoder, 130 + deviceApi, messageService, frameEncoder,
126 connectionManager, configCacheService, pendingRequestManager, 131 connectionManager, configCacheService, pendingRequestManager,
127 pollScheduler, deviceService, serverId); 132 pollScheduler, deviceService, serverId);
128 133
129 // 初始化下行消息订阅者 134 // 初始化下行消息订阅者
130 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class); 135 IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
131 IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler( 136 IotModbusTcpSlaveDownstreamHandler downstreamHandler = new IotModbusTcpSlaveDownstreamHandler(
132 - connectionManager, configCacheService, dataConverter, frameEncoder); 137 + connectionManager, configCacheService, frameEncoder, transactionIdCounter);
133 this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber( 138 this.downstreamSubscriber = new IotModbusTcpSlaveDownstreamSubscriber(
134 this, downstreamHandler, messageBus); 139 this, downstreamHandler, messageBus);
135 } 140 }
@@ -284,6 +289,13 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol { @@ -284,6 +289,13 @@ public class IotModbusTcpSlaveProtocol implements IotProtocol {
284 pollScheduler.stopPolling(info.getDeviceId()); 289 pollScheduler.stopPolling(info.getDeviceId());
285 pendingRequestManager.removeDevice(info.getDeviceId()); 290 pendingRequestManager.removeDevice(info.getDeviceId());
286 configCacheService.removeConfig(info.getDeviceId()); 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 log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]", 299 log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]",
288 info.getDeviceId(), socket.remoteAddress()); 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,9 +8,7 @@ import lombok.extern.slf4j.Slf4j;
8 import java.nio.charset.StandardCharsets; 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 * @author 芋道源码 13 * @author 芋道源码
16 */ 14 */
@@ -30,11 +28,11 @@ public class IotModbusFrameEncoder { @@ -30,11 +28,11 @@ public class IotModbusFrameEncoder {
30 * @param startAddress 起始寄存器地址 28 * @param startAddress 起始寄存器地址
31 * @param quantity 寄存器数量 29 * @param quantity 寄存器数量
32 * @param format 帧格式 30 * @param format 帧格式
33 - * @param transactionId 事务 ID(TCP 模式下使用) 31 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null)
34 * @return 编码后的字节数组 32 * @return 编码后的字节数组
35 */ 33 */
36 public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity, 34 public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity,
37 - IotModbusFrameFormatEnum format, int transactionId) { 35 + IotModbusFrameFormatEnum format, Integer transactionId) {
38 // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)] 36 // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)]
39 byte[] pdu = new byte[5]; 37 byte[] pdu = new byte[5];
40 pdu[0] = (byte) functionCode; 38 pdu[0] = (byte) functionCode;
@@ -48,16 +46,23 @@ public class IotModbusFrameEncoder { @@ -48,16 +46,23 @@ public class IotModbusFrameEncoder {
48 /** 46 /**
49 * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05) 47 * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05)
50 * 48 *
  49 + * DONE @AI:【from codex】【高】FC05 写线圈时,value 已转换为 Modbus 标准值(非0 → 0xFF00,0 → 0x0000);
  50 + * 新增 encodeWriteMultipleCoilsRequest 方法用于 FC15 编码(按 bit 打包)。
  51 + *
51 * @param slaveId 从站地址 52 * @param slaveId 从站地址
52 * @param functionCode 功能码 53 * @param functionCode 功能码
53 * @param address 寄存器地址 54 * @param address 寄存器地址
54 * @param value 值 55 * @param value 值
55 * @param format 帧格式 56 * @param format 帧格式
56 - * @param transactionId 事务 ID 57 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null)
57 * @return 编码后的字节数组 58 * @return 编码后的字节数组
58 */ 59 */
59 public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value, 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 // PDU: [FC(1)] [Address(2)] [Value(2)] 66 // PDU: [FC(1)] [Address(2)] [Value(2)]
62 byte[] pdu = new byte[5]; 67 byte[] pdu = new byte[5];
63 pdu[0] = (byte) functionCode; 68 pdu[0] = (byte) functionCode;
@@ -75,11 +80,11 @@ public class IotModbusFrameEncoder { @@ -75,11 +80,11 @@ public class IotModbusFrameEncoder {
75 * @param address 起始地址 80 * @param address 起始地址
76 * @param values 值数组 81 * @param values 值数组
77 * @param format 帧格式 82 * @param format 帧格式
78 - * @param transactionId 事务 ID 83 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null)
79 * @return 编码后的字节数组 84 * @return 编码后的字节数组
80 */ 85 */
81 public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values, 86 public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values,
82 - IotModbusFrameFormatEnum format, int transactionId) { 87 + IotModbusFrameFormatEnum format, Integer transactionId) {
83 // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)] 88 // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)]
84 int quantity = values.length; 89 int quantity = values.length;
85 int byteCount = quantity * 2; 90 int byteCount = quantity * 2;
@@ -98,16 +103,49 @@ public class IotModbusFrameEncoder { @@ -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 * @param slaveId 从站地址 141 * @param slaveId 从站地址
104 * @param jsonData JSON 数据 142 * @param jsonData JSON 数据
105 * @param format 帧格式 143 * @param format 帧格式
106 - * @param transactionId 事务 ID 144 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null)
107 * @return 编码后的字节数组 145 * @return 编码后的字节数组
108 */ 146 */
109 public byte[] encodeCustomFrame(int slaveId, String jsonData, 147 public byte[] encodeCustomFrame(int slaveId, String jsonData,
110 - IotModbusFrameFormatEnum format, int transactionId) { 148 + IotModbusFrameFormatEnum format, Integer transactionId) {
111 byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8); 149 byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8);
112 // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)] 150 // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)]
113 byte[] pdu = new byte[2 + jsonBytes.length]; 151 byte[] pdu = new byte[2 + jsonBytes.length];
@@ -125,12 +163,12 @@ public class IotModbusFrameEncoder { @@ -125,12 +163,12 @@ public class IotModbusFrameEncoder {
125 * @param slaveId 从站地址 163 * @param slaveId 从站地址
126 * @param pdu PDU 数据(含 functionCode) 164 * @param pdu PDU 数据(含 functionCode)
127 * @param format 帧格式 165 * @param format 帧格式
128 - * @param transactionId 事务 ID(TCP 模式下使用) 166 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式可为 null)
129 * @return 完整帧字节数组 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 if (format == IotModbusFrameFormatEnum.MODBUS_TCP) { 170 if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
133 - return wrapTcpFrame(slaveId, pdu, transactionId); 171 + return wrapTcpFrame(slaveId, pdu, transactionId != null ? transactionId : 0);
134 } else { 172 } else {
135 return wrapRtuFrame(slaveId, pdu); 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,13 +7,11 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; 8 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;  
11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
12 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
13 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; 12 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
14 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
15 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; 14 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
16 -import lombok.RequiredArgsConstructor;  
17 import lombok.extern.slf4j.Slf4j; 15 import lombok.extern.slf4j.Slf4j;
18 16
19 import java.util.Map; 17 import java.util.Map;
@@ -28,19 +26,27 @@ import java.util.concurrent.atomic.AtomicInteger; @@ -28,19 +26,27 @@ import java.util.concurrent.atomic.AtomicInteger;
28 * 26 *
29 * @author 芋道源码 27 * @author 芋道源码
30 */ 28 */
31 -@RequiredArgsConstructor  
32 @Slf4j 29 @Slf4j
33 public class IotModbusTcpSlaveDownstreamHandler { 30 public class IotModbusTcpSlaveDownstreamHandler {
34 31
35 private final IotModbusTcpSlaveConnectionManager connectionManager; 32 private final IotModbusTcpSlaveConnectionManager connectionManager;
36 private final IotModbusTcpSlaveConfigCacheService configCacheService; 33 private final IotModbusTcpSlaveConfigCacheService configCacheService;
37 - private final IotModbusDataConverter dataConverter;  
38 private final IotModbusFrameEncoder frameEncoder; 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,12 +104,15 @@ public class IotModbusTcpSlaveDownstreamHandler {
98 private void writeProperty(Long deviceId, ConnectionInfo connInfo, 104 private void writeProperty(Long deviceId, ConnectionInfo connInfo,
99 IotModbusPointRespDTO point, Object value) { 105 IotModbusPointRespDTO point, Object value) {
100 // 1.1 转换属性值为原始值 106 // 1.1 转换属性值为原始值
101 - int[] rawValues = dataConverter.convertToRawValues(value, point); 107 + int[] rawValues = IotModbusUtils.convertToRawValues(value, point);
102 108
103 // 1.2 确定帧格式和事务 ID 109 // 1.2 确定帧格式和事务 ID
104 IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); 110 IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
105 Assert.notNull(frameFormat, "连接帧格式不能为空"); 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 int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; 116 int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
108 // 1.3 编码写请求 117 // 1.3 编码写请求
109 byte[] data; 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,7 +19,6 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
19 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 19 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
20 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; 20 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
21 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 21 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
22 -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;  
23 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils; 22 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
24 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; 23 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
25 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; 24 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
@@ -56,7 +55,6 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -56,7 +55,6 @@ public class IotModbusTcpSlaveUpstreamHandler {
56 55
57 private final IotDeviceCommonApi deviceApi; 56 private final IotDeviceCommonApi deviceApi;
58 private final IotDeviceMessageService messageService; 57 private final IotDeviceMessageService messageService;
59 - private final IotModbusDataConverter dataConverter;  
60 private final IotModbusFrameEncoder frameEncoder; 58 private final IotModbusFrameEncoder frameEncoder;
61 private final IotModbusTcpSlaveConnectionManager connectionManager; 59 private final IotModbusTcpSlaveConnectionManager connectionManager;
62 private final IotModbusTcpSlaveConfigCacheService configCacheService; 60 private final IotModbusTcpSlaveConfigCacheService configCacheService;
@@ -67,7 +65,6 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -67,7 +65,6 @@ public class IotModbusTcpSlaveUpstreamHandler {
67 65
68 public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi, 66 public IotModbusTcpSlaveUpstreamHandler(IotDeviceCommonApi deviceApi,
69 IotDeviceMessageService messageService, 67 IotDeviceMessageService messageService,
70 - IotModbusDataConverter dataConverter,  
71 IotModbusFrameEncoder frameEncoder, 68 IotModbusFrameEncoder frameEncoder,
72 IotModbusTcpSlaveConnectionManager connectionManager, 69 IotModbusTcpSlaveConnectionManager connectionManager,
73 IotModbusTcpSlaveConfigCacheService configCacheService, 70 IotModbusTcpSlaveConfigCacheService configCacheService,
@@ -77,7 +74,6 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -77,7 +74,6 @@ public class IotModbusTcpSlaveUpstreamHandler {
77 String serverId) { 74 String serverId) {
78 this.deviceApi = deviceApi; 75 this.deviceApi = deviceApi;
79 this.messageService = messageService; 76 this.messageService = messageService;
80 - this.dataConverter = dataConverter;  
81 this.frameEncoder = frameEncoder; 77 this.frameEncoder = frameEncoder;
82 this.connectionManager = connectionManager; 78 this.connectionManager = connectionManager;
83 this.configCacheService = configCacheService; 79 this.configCacheService = configCacheService;
@@ -175,6 +171,7 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -175,6 +171,7 @@ public class IotModbusTcpSlaveUpstreamHandler {
175 // 2.3 获取设备信息 171 // 2.3 获取设备信息
176 IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); 172 IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
177 Assert.notNull(device, "设备不存在"); 173 Assert.notNull(device, "设备不存在");
  174 + // TODO @AI:2.4 必须找到连接配置;
178 175
179 // 3.1 注册连接 176 // 3.1 注册连接
180 ConnectionInfo connectionInfo = new ConnectionInfo() 177 ConnectionInfo connectionInfo = new ConnectionInfo()
@@ -213,7 +210,7 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -213,7 +210,7 @@ public class IotModbusTcpSlaveUpstreamHandler {
213 .put("message", message) 210 .put("message", message)
214 .build(); 211 .build();
215 byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response), 212 byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response),
216 - frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); 213 + frameFormat, frame.getTransactionId());
217 connectionManager.sendToSocket(socket, data); 214 connectionManager.sendToSocket(socket, data);
218 } 215 }
219 216
@@ -258,7 +255,7 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -258,7 +255,7 @@ public class IotModbusTcpSlaveUpstreamHandler {
258 } 255 }
259 256
260 // 3.1 点位翻译 257 // 3.1 点位翻译
261 - Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); 258 + Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValues, point);
262 // 3.2 上报属性 259 // 3.2 上报属性
263 Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue); 260 Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue);
264 IotDeviceMessage message = IotDeviceMessage.requestOf( 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,12 +66,15 @@ public class IotModbusTcpSlaveConfigCacheService {
66 log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e); 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 // TODO @芋艿:测试完成后移除 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,9 +107,11 @@ public class IotModbusTcpSlaveConfigCacheService {
104 allConfigs = new ArrayList<>(); 107 allConfigs = new ArrayList<>();
105 } 108 }
106 109
107 - // 2. 追加 Mock 测试数据 110 + // 2. 追加 Mock 测试数据(仅 mockEnabled=true 时)
108 // TODO @芋艿:测试完成后移除 111 // TODO @芋艿:测试完成后移除
109 - allConfigs.addAll(buildMockConfigs()); 112 + if (true) {
  113 + allConfigs.addAll(buildMockConfigs());
  114 + }
110 115
111 // 3. 只保留已连接设备的配置,更新缓存 116 // 3. 只保留已连接设备的配置,更新缓存
112 List<IotModbusDeviceConfigRespDTO> connectedConfigs = new ArrayList<>(); 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,6 +66,21 @@ public class IotModbusTcpSlaveConnectionManager {
66 * 注册已认证的连接 66 * 注册已认证的连接
67 */ 67 */
68 public void registerConnection(NetSocket socket, ConnectionInfo info) { 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 connectionMap.put(socket, info); 84 connectionMap.put(socket, info);
70 deviceSocketMap.put(info.getDeviceId(), socket); 85 deviceSocketMap.put(info.getDeviceId(), socket);
71 log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]", 86 log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]",
@@ -100,8 +115,14 @@ public class IotModbusTcpSlaveConnectionManager { @@ -100,8 +115,14 @@ public class IotModbusTcpSlaveConnectionManager {
100 public ConnectionInfo removeConnection(NetSocket socket) { 115 public ConnectionInfo removeConnection(NetSocket socket) {
101 ConnectionInfo info = connectionMap.remove(socket); 116 ConnectionInfo info = connectionMap.remove(socket);
102 if (info != null && info.getDeviceId() != null) { 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 return info; 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,13 +118,13 @@ public class IotModbusTcpSlavePendingRequestManager {
118 for (Map.Entry<Long, Deque<PendingRequest>> entry : pendingRequests.entrySet()) { 118 for (Map.Entry<Long, Deque<PendingRequest>> entry : pendingRequests.entrySet()) {
119 Deque<PendingRequest> queue = entry.getValue(); 119 Deque<PendingRequest> queue = entry.getValue();
120 int removed = 0; 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 if (removed > 0) { 129 if (removed > 0) {
130 log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed); 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,11 +10,11 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotM
10 import io.vertx.core.Vertx; 10 import io.vertx.core.Vertx;
11 import lombok.AllArgsConstructor; 11 import lombok.AllArgsConstructor;
12 import lombok.Data; 12 import lombok.Data;
13 -import lombok.RequiredArgsConstructor;  
14 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
15 14
16 import java.util.*; 15 import java.util.*;
17 import java.util.concurrent.ConcurrentHashMap; 16 import java.util.concurrent.ConcurrentHashMap;
  17 +import java.util.concurrent.ConcurrentLinkedQueue;
18 import java.util.concurrent.atomic.AtomicInteger; 18 import java.util.concurrent.atomic.AtomicInteger;
19 19
20 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; 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,10 +27,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
27 * 1. 编码 Modbus 读请求帧 27 * 1. 编码 Modbus 读请求帧
28 * 2. 通过 ConnectionManager 发送到设备的 TCP 连接 28 * 2. 通过 ConnectionManager 发送到设备的 TCP 连接
29 * 3. 将请求注册到 PendingRequestManager,等待设备响应 29 * 3. 将请求注册到 PendingRequestManager,等待设备响应
  30 + * <p>
  31 + * 闭包只捕获 deviceId + pointId,运行时从 configCacheService 获取最新 config 和 point,
  32 + * 避免闭包捕获旧快照导致上报消息用旧身份的问题。
30 * 33 *
31 * @author 芋道源码 34 * @author 芋道源码
32 */ 35 */
33 -@RequiredArgsConstructor  
34 @Slf4j 36 @Slf4j
35 public class IotModbusTcpSlavePollScheduler { 37 public class IotModbusTcpSlavePollScheduler {
36 38
@@ -38,7 +40,17 @@ public class IotModbusTcpSlavePollScheduler { @@ -38,7 +40,17 @@ public class IotModbusTcpSlavePollScheduler {
38 private final IotModbusTcpSlaveConnectionManager connectionManager; 40 private final IotModbusTcpSlaveConnectionManager connectionManager;
39 private final IotModbusFrameEncoder frameEncoder; 41 private final IotModbusFrameEncoder frameEncoder;
40 private final IotModbusTcpSlavePendingRequestManager pendingRequestManager; 42 private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
  43 + private final IotModbusTcpSlaveConfigCacheService configCacheService;
41 private final int requestTimeout; 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 * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo) 56 * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
@@ -46,9 +58,33 @@ public class IotModbusTcpSlavePollScheduler { @@ -46,9 +58,33 @@ public class IotModbusTcpSlavePollScheduler {
46 private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>(); 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,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 public void updatePolling(IotModbusDeviceConfigRespDTO config) { 117 public void updatePolling(IotModbusDeviceConfigRespDTO config) {
75 Long deviceId = config.getDeviceId(); 118 Long deviceId = config.getDeviceId();
@@ -101,7 +144,7 @@ public class IotModbusTcpSlavePollScheduler { @@ -101,7 +144,7 @@ public class IotModbusTcpSlavePollScheduler {
101 PointTimerInfo existingTimer = currentTimers.get(pointId); 144 PointTimerInfo existingTimer = currentTimers.get(pointId);
102 // 3.1 新增点位:创建定时器 145 // 3.1 新增点位:创建定时器
103 if (existingTimer == null) { 146 if (existingTimer == null) {
104 - Long timerId = createPollTimer(config, point); 147 + Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
105 if (timerId != null) { 148 if (timerId != null) {
106 currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); 149 currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
107 log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]", 150 log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
@@ -110,7 +153,7 @@ public class IotModbusTcpSlavePollScheduler { @@ -110,7 +153,7 @@ public class IotModbusTcpSlavePollScheduler {
110 } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) { 153 } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
111 // 3.2 pollInterval 变化:重建定时器 154 // 3.2 pollInterval 变化:重建定时器
112 vertx.cancelTimer(existingTimer.getTimerId()); 155 vertx.cancelTimer(existingTimer.getTimerId());
113 - Long timerId = createPollTimer(config, point); 156 + Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
114 if (timerId != null) { 157 if (timerId != null) {
115 currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval)); 158 currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
116 log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]", 159 log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
@@ -119,54 +162,144 @@ public class IotModbusTcpSlavePollScheduler { @@ -119,54 +162,144 @@ public class IotModbusTcpSlavePollScheduler {
119 currentTimers.remove(pointId); 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 return null; 176 return null;
132 } 177 }
133 - return vertx.setPeriodic(point.getPollInterval(), timerId -> { 178 + return vertx.setPeriodic(pollInterval, timerId -> {
134 try { 179 try {
135 - pollPoint(config, point); 180 + submitPollRequest(deviceId, pointId);
136 } catch (Exception e) { 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 ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(deviceId); 282 ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(deviceId);
150 if (connInfo == null) { 283 if (connInfo == null) {
151 log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId); 284 log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId);
152 return; 285 return;
153 } 286 }
154 287
155 - // 2.1 确定帧格式和事务 ID 288 + // 3.1 确定帧格式和事务 ID
156 IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); 289 IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
157 if (frameFormat == null) { 290 if (frameFormat == null) {
158 log.warn("[pollPoint][设备 {} 帧格式为空,跳过轮询]", deviceId); 291 log.warn("[pollPoint][设备 {} 帧格式为空,跳过轮询]", deviceId);
159 return; 292 return;
160 } 293 }
161 - // TODO @AI:是不是得按照设备递增?  
162 Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP 294 Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP
163 ? (transactionIdCounter.incrementAndGet() & 0xFFFF) 295 ? (transactionIdCounter.incrementAndGet() & 0xFFFF)
164 : null; 296 : null;
  297 + // TODO @AI:这里断言必须非空!
165 int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; 298 int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
166 - // 2.2 编码读请求 299 + // 3.2 编码读请求
167 byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(), 300 byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(),
168 point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId); 301 point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId);
169 - // 2.3 注册 PendingRequest 302 + // 3.3 注册 PendingRequest
170 PendingRequest pendingRequest = new PendingRequest( 303 PendingRequest pendingRequest = new PendingRequest(
171 deviceId, point.getId(), point.getIdentifier(), 304 deviceId, point.getId(), point.getIdentifier(),
172 slaveId, point.getFunctionCode(), 305 slaveId, point.getFunctionCode(),
@@ -175,13 +308,15 @@ public class IotModbusTcpSlavePollScheduler { @@ -175,13 +308,15 @@ public class IotModbusTcpSlavePollScheduler {
175 System.currentTimeMillis() + requestTimeout); 308 System.currentTimeMillis() + requestTimeout);
176 pendingRequestManager.addRequest(pendingRequest); 309 pendingRequestManager.addRequest(pendingRequest);
177 310
178 - // 3. 发送读请求 311 + // 4. 发送读请求
179 connectionManager.sendToDevice(deviceId, data); 312 connectionManager.sendToDevice(deviceId, data);
180 log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]", 313 log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]",
181 deviceId, point.getIdentifier(), point.getFunctionCode(), 314 deviceId, point.getIdentifier(), point.getFunctionCode(),
182 point.getRegisterAddress(), point.getRegisterCount()); 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,6 +328,10 @@ public class IotModbusTcpSlavePollScheduler {
193 for (PointTimerInfo timerInfo : timers.values()) { 328 for (PointTimerInfo timerInfo : timers.values()) {
194 vertx.cancelTimer(timerInfo.getTimerId()); 329 vertx.cancelTimer(timerInfo.getTimerId());
195 } 330 }
  331 + // 清理请求队列
  332 + deviceRequestQueues.remove(deviceId);
  333 + deviceLastRequestTime.remove(deviceId);
  334 + deviceDelayTimerActive.remove(deviceId);
196 log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size()); 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,6 +25,7 @@ import java.nio.ByteOrder;
25 import java.util.concurrent.CompletableFuture; 25 import java.util.concurrent.CompletableFuture;
26 import java.util.concurrent.TimeUnit; 26 import java.util.concurrent.TimeUnit;
27 27
  28 +// TODO @芋艿:【晚点改】单测需要简化!
28 /** 29 /**
29 * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试) 30 * IoT Modbus TCP Slave 协议集成测试 — MODBUS_TCP 帧格式(手动测试)
30 * 31 *
@@ -123,6 +124,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest { @@ -123,6 +124,7 @@ public class IotModbusTcpSlaveModbusTcpIntegrationTest {
123 // 1. 先认证 124 // 1. 先认证
124 IotModbusFrame authResponse = authenticate(socket); 125 IotModbusFrame authResponse = authenticate(socket);
125 log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData()); 126 log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
  127 + // TODO @AI:这里断言下,认证必须成功!
126 128
127 // 2. 设置持续监听:每收到一个读请求,自动回复 129 // 2. 设置持续监听:每收到一个读请求,自动回复
128 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]"); 130 log.info("[testPollingResponse][开始持续监听网关下发的读请求...]");