Commit c608b81c4e9e347d42a56a0847cf94de449ee4f7

Authored by YunaiV
1 parent e13cd545

feat(iot):modbus-tcp-slave 优化大量代码,主要是 polling 轮询的复用;

Showing 24 changed files with 373 additions and 566 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java
... ... @@ -17,7 +17,7 @@ import lombok.extern.slf4j.Slf4j;
17 17 */
18 18 @AllArgsConstructor
19 19 @Slf4j
20   -public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
  20 +public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
21 21  
22 22 private final IotProtocol protocol;
23 23  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) {
18 18 super(protocol, messageBus);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotEmqxDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream;
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
... ... @@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j;
13 13 */
14 14  
15 15 @Slf4j
16   -public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  16 +public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
17 17  
18 18 public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) {
19 19 super(protocol, messageBus);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  6 +import io.vertx.core.Vertx;
  7 +import lombok.AllArgsConstructor;
  8 +import lombok.Data;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +import java.util.*;
  12 +import java.util.concurrent.ConcurrentHashMap;
  13 +import java.util.concurrent.ConcurrentLinkedQueue;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  16 +
  17 +/**
  18 + * Modbus 轮询调度器基类
  19 + * <p>
  20 + * 封装通用的定时器管理、per-device 请求队列限速逻辑。
  21 + * 子类只需实现 {@link #pollPoint(Long, Long)} 定义具体的轮询动作。
  22 + * <p>
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +@Slf4j
  27 +public abstract class AbstractIotModbusPollScheduler {
  28 +
  29 + protected final Vertx vertx;
  30 +
  31 + /**
  32 + * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积
  33 + */
  34 + private static final long MIN_REQUEST_INTERVAL = 1000;
  35 +
  36 + /**
  37 + * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
  38 + */
  39 + private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
  40 +
  41 + /**
  42 + * per-device 请求队列:deviceId -> 待执行请求队列
  43 + */
  44 + private final Map<Long, Queue<Runnable>> deviceRequestQueues = new ConcurrentHashMap<>();
  45 + /**
  46 + * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs
  47 + */
  48 + private final Map<Long, Long> deviceLastRequestTime = new ConcurrentHashMap<>();
  49 + /**
  50 + * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待
  51 + */
  52 + private final Map<Long, Boolean> deviceDelayTimerActive = new ConcurrentHashMap<>();
  53 +
  54 + protected AbstractIotModbusPollScheduler(Vertx vertx) {
  55 + this.vertx = vertx;
  56 + }
  57 +
  58 + /**
  59 + * 点位定时器信息
  60 + */
  61 + @Data
  62 + @AllArgsConstructor
  63 + private static class PointTimerInfo {
  64 +
  65 + /**
  66 + * Vert.x 定时器 ID
  67 + */
  68 + private Long timerId;
  69 + /**
  70 + * 轮询间隔(用于判断是否需要更新定时器)
  71 + */
  72 + private Integer pollInterval;
  73 +
  74 + }
  75 +
  76 + // ========== 轮询管理 ==========
  77 +
  78 + /**
  79 + * 更新轮询任务(增量更新)
  80 + *
  81 + * 1. 【删除】点位:停止对应的轮询定时器
  82 + * 2. 【新增】点位:创建对应的轮询定时器
  83 + * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器
  84 + * 【修改】其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point)
  85 + */
  86 + public void updatePolling(IotModbusDeviceConfigRespDTO config) {
  87 + Long deviceId = config.getDeviceId();
  88 + List<IotModbusPointRespDTO> newPoints = config.getPoints();
  89 + Map<Long, PointTimerInfo> currentTimers = devicePointTimers
  90 + .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
  91 + // 1.1 计算新配置中的点位 ID 集合
  92 + Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
  93 + // 1.2 计算删除的点位 ID 集合
  94 + Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
  95 + removedPointIds.removeAll(newPointIds);
  96 +
  97 + // 2. 处理删除的点位:停止不再存在的定时器
  98 + for (Long pointId : removedPointIds) {
  99 + PointTimerInfo timerInfo = currentTimers.remove(pointId);
  100 + if (timerInfo != null) {
  101 + vertx.cancelTimer(timerInfo.getTimerId());
  102 + log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
  103 + }
  104 + }
  105 +
  106 + // 3. 处理新增和修改的点位
  107 + if (CollUtil.isEmpty(newPoints)) {
  108 + return;
  109 + }
  110 + for (IotModbusPointRespDTO point : newPoints) {
  111 + Long pointId = point.getId();
  112 + Integer newPollInterval = point.getPollInterval();
  113 + PointTimerInfo existingTimer = currentTimers.get(pointId);
  114 + // 3.1 新增点位:创建定时器
  115 + if (existingTimer == null) {
  116 + Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
  117 + if (timerId != null) {
  118 + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
  119 + log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
  120 + deviceId, pointId, newPollInterval);
  121 + }
  122 + } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
  123 + // 3.2 pollInterval 变化:重建定时器
  124 + vertx.cancelTimer(existingTimer.getTimerId());
  125 + Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
  126 + if (timerId != null) {
  127 + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
  128 + log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
  129 + deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
  130 + } else {
  131 + currentTimers.remove(pointId);
  132 + }
  133 + }
  134 + // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置
  135 + }
  136 + }
  137 +
  138 + /**
  139 + * 创建轮询定时器
  140 + */
  141 + private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) {
  142 + if (pollInterval == null || pollInterval <= 0) {
  143 + return null;
  144 + }
  145 + return vertx.setPeriodic(pollInterval, timerId -> {
  146 + try {
  147 + submitPollRequest(deviceId, pointId);
  148 + } catch (Exception e) {
  149 + log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e);
  150 + }
  151 + });
  152 + }
  153 +
  154 + // ========== 请求队列(per-device 限速) ==========
  155 +
  156 + /**
  157 + * 提交轮询请求到设备请求队列(保证同设备请求间隔)
  158 + */
  159 + private void submitPollRequest(Long deviceId, Long pointId) {
  160 + // 1. 【重要】将请求添加到设备的请求队列
  161 + Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>());
  162 + queue.offer(() -> pollPoint(deviceId, pointId));
  163 +
  164 + // 2. 处理设备请求队列(如果没有延迟 timer 在等待)
  165 + processDeviceQueue(deviceId);
  166 + }
  167 +
  168 + /**
  169 + * 处理设备请求队列
  170 + */
  171 + private void processDeviceQueue(Long deviceId) {
  172 + Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
  173 + if (CollUtil.isEmpty(queue)) {
  174 + return;
  175 + }
  176 + // 检查是否已有延迟 timer 在等待
  177 + if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) {
  178 + return;
  179 + }
  180 +
  181 + // 不满足间隔要求,延迟执行
  182 + long now = System.currentTimeMillis();
  183 + long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L);
  184 + long elapsed = now - lastTime;
  185 + if (elapsed < MIN_REQUEST_INTERVAL) {
  186 + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed);
  187 + return;
  188 + }
  189 +
  190 + // 满足间隔要求,立即执行
  191 + Runnable task = queue.poll();
  192 + if (task == null) {
  193 + return;
  194 + }
  195 + deviceLastRequestTime.put(deviceId, now);
  196 + task.run();
  197 + // 继续处理队列中的下一个(如果有的话,需要延迟)
  198 + if (CollUtil.isNotEmpty(queue)) {
  199 + scheduleNextRequest(deviceId);
  200 + }
  201 + }
  202 +
  203 + private void scheduleNextRequest(Long deviceId) {
  204 + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL);
  205 + }
  206 +
  207 + private void scheduleNextRequest(Long deviceId, long delayMs) {
  208 + deviceDelayTimerActive.put(deviceId, true);
  209 + vertx.setTimer(delayMs, id -> {
  210 + deviceDelayTimerActive.put(deviceId, false);
  211 + Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
  212 + if (CollUtil.isEmpty(queue)) {
  213 + return;
  214 + }
  215 +
  216 + // 满足间隔要求,立即执行
  217 + Runnable task = queue.poll();
  218 + if (task == null) {
  219 + return;
  220 + }
  221 + deviceLastRequestTime.put(deviceId, System.currentTimeMillis());
  222 + task.run();
  223 + // 继续处理队列中的下一个(如果有的话,需要延迟)
  224 + if (CollUtil.isNotEmpty(queue)) {
  225 + scheduleNextRequest(deviceId);
  226 + }
  227 + });
  228 + }
  229 +
  230 + // ========== 轮询执行 ==========
  231 +
  232 + /**
  233 + * 轮询单个点位(子类实现具体的读取逻辑)
  234 + *
  235 + * @param deviceId 设备 ID
  236 + * @param pointId 点位 ID
  237 + */
  238 + protected abstract void pollPoint(Long deviceId, Long pointId);
  239 +
  240 + // ========== 停止 ==========
  241 +
  242 + /**
  243 + * 停止设备的轮询
  244 + */
  245 + public void stopPolling(Long deviceId) {
  246 + Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
  247 + if (CollUtil.isEmpty(timers)) {
  248 + return;
  249 + }
  250 + for (PointTimerInfo timerInfo : timers.values()) {
  251 + vertx.cancelTimer(timerInfo.getTimerId());
  252 + }
  253 + // 清理请求队列
  254 + deviceRequestQueues.remove(deviceId);
  255 + deviceLastRequestTime.remove(deviceId);
  256 + deviceDelayTimerActive.remove(deviceId);
  257 + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
  258 + }
  259 +
  260 + /**
  261 + * 停止所有轮询
  262 + */
  263 + public void stopAll() {
  264 + for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
  265 + stopPolling(deviceId);
  266 + }
  267 + }
  268 +
  269 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ArrayUtil;
... ... @@ -34,7 +34,7 @@ import java.nio.ByteOrder;
34 34 */
35 35 @UtilityClass
36 36 @Slf4j
37   -public class IotModbusUtils {
  37 +public class IotModbusCommonUtils {
38 38  
39 39 /** FC01: 读线圈 */
40 40 public static final int FC_READ_COILS = 1;
... ... @@ -512,4 +512,15 @@ public class IotModbusUtils {
512 512 return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
513 513 }
514 514  
  515 + /**
  516 + * 根据点位 ID 查找点位配置
  517 + *
  518 + * @param config 设备 Modbus 配置
  519 + * @param pointId 点位 ID
  520 + * @return 匹配的点位配置,未找到返回 null
  521 + */
  522 + public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) {
  523 + return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId));
  524 + }
  525 +
515 526 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClientUtils.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpMasterUtils.java
1   -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client;
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
4 4 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
... ... @@ -12,7 +12,7 @@ import io.vertx.core.Future;
12 12 import lombok.experimental.UtilityClass;
13 13 import lombok.extern.slf4j.Slf4j;
14 14  
15   -import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils.*;
  15 +import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.*;
16 16  
17 17 /**
18 18 * IoT Modbus TCP 客户端工具类
... ... @@ -26,7 +26,7 @@ import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModb
26 26 */
27 27 @UtilityClass
28 28 @Slf4j
29   -public class IotModbusTcpClientUtils {
  29 +public class IotModbusTcpMasterUtils {
30 30  
31 31 /**
32 32 * 读取 Modbus 数据
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java
... ... @@ -4,8 +4,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
4 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
5 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClientUtils;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService;
10 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
11 11 import lombok.RequiredArgsConstructor;
... ... @@ -57,13 +57,13 @@ public class IotModbusTcpDownstreamHandler {
57 57 String identifier = entry.getKey();
58 58 Object value = entry.getValue();
59 59 // 2.1 查找对应的点位配置
60   - IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier);
  60 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier);
61 61 if (point == null) {
62 62 log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
63 63 continue;
64 64 }
65 65 // 2.2 检查是否支持写操作
66   - if (!IotModbusUtils.isWritable(point.getFunctionCode())) {
  66 + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) {
67 67 log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
68 68 continue;
69 69 }
... ... @@ -91,9 +91,9 @@ public class IotModbusTcpDownstreamHandler {
91 91 }
92 92  
93 93 // 2.1 转换属性值为原始值
94   - int[] rawValues = IotModbusUtils.convertToRawValues(value, point);
  94 + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point);
95 95 // 2.2 执行 Modbus 写入
96   - IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues)
  96 + IotModbusTcpMasterUtils.write(connection, slaveId, point, rawValues)
97 97 .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
98 98 config.getDeviceId(), point.getIdentifier(), value))
99 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/downstream/IotModbusTcpDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.do
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotModbusTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotModbusTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotModbusTcpDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/upstream/IotModbusTcpUpstreamHandler.java
... ... @@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
9 9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
10 10 import lombok.extern.slf4j.Slf4j;
11 11  
... ... @@ -40,7 +40,7 @@ public class IotModbusTcpUpstreamHandler {
40 40 int[] rawValue) {
41 41 try {
42 42 // 1.1 转换原始值为物模型属性值
43   - Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValue, point);
  43 + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point);
44 44 log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]",
45 45 config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue);
46 46 // 1.2 构造属性上报消息
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/manager/IotModbusTcpPollScheduler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
4 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClientUtils;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpMasterUtils;
7 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.upstream.IotModbusTcpUpstreamHandler;
8 11 import io.vertx.core.Vertx;
9   -import lombok.AllArgsConstructor;
10   -import lombok.Data;
11 12 import lombok.extern.slf4j.Slf4j;
12 13  
13   -import java.util.*;
14   -import java.util.concurrent.ConcurrentHashMap;
15   -import java.util.concurrent.ConcurrentLinkedQueue;
16   -
17   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
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 也要!
20 14 /**
21 15 * IoT Modbus TCP Master 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果
22 16 *
23 17 * @author 芋道源码
24 18 */
25 19 @Slf4j
26   -public class IotModbusTcpPollScheduler {
  20 +public class IotModbusTcpPollScheduler extends AbstractIotModbusPollScheduler {
27 21  
28   - private final Vertx vertx;
29 22 private final IotModbusTcpConnectionManager connectionManager;
30 23 private final IotModbusTcpUpstreamHandler upstreamHandler;
31 24 private final IotModbusTcpConfigCacheService configCacheService;
32 25  
33   - /**
34   - * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积
35   - */
36   - private static final long MIN_REQUEST_INTERVAL = 100;
37   -
38   - /**
39   - * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
40   - */
41   - private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
42   -
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 26 public IotModbusTcpPollScheduler(Vertx vertx,
59 27 IotModbusTcpConnectionManager connectionManager,
60 28 IotModbusTcpUpstreamHandler upstreamHandler,
61 29 IotModbusTcpConfigCacheService configCacheService) {
62   - this.vertx = vertx;
  30 + super(vertx);
63 31 this.connectionManager = connectionManager;
64 32 this.upstreamHandler = upstreamHandler;
65 33 this.configCacheService = configCacheService;
66 34 }
67 35  
68   - // ========== 点位定时器 ==========
69   -
70   - /**
71   - * 点位定时器信息
72   - */
73   - @Data
74   - @AllArgsConstructor
75   - private static class PointTimerInfo {
76   -
77   - /**
78   - * Vert.x 定时器 ID
79   - */
80   - private Long timerId;
81   - /**
82   - * 轮询间隔(用于判断是否需要更新定时器)
83   - */
84   - private Integer pollInterval;
85   -
86   - }
87   -
88   - // ========== 轮询管理 ==========
89   -
90   - /**
91   - * 更新轮询任务(增量更新)
92   - *
93   - * 1. 【删除】点位:停止对应的轮询定时器
94   - * 2. 【新增】点位:创建对应的轮询定时器
95   - * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器
96   - * 4. 其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point)
97   - */
98   - public void updatePolling(IotModbusDeviceConfigRespDTO config) {
99   - Long deviceId = config.getDeviceId();
100   - List<IotModbusPointRespDTO> newPoints = config.getPoints();
101   - Map<Long, PointTimerInfo> currentTimers = devicePointTimers
102   - .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
103   - // 1.1 计算新配置中的点位 ID 集合
104   - Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
105   - // 1.2 计算删除的点位 ID 集合
106   - Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
107   - removedPointIds.removeAll(newPointIds);
108   -
109   - // 2. 处理删除的点位:停止不再存在的定时器
110   - for (Long pointId : removedPointIds) {
111   - PointTimerInfo timerInfo = currentTimers.remove(pointId);
112   - if (timerInfo != null) {
113   - vertx.cancelTimer(timerInfo.getTimerId());
114   - log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
115   - }
116   - }
117   -
118   - // 3. 处理新增和修改的点位
119   - if (CollUtil.isEmpty(newPoints)) {
120   - return;
121   - }
122   - for (IotModbusPointRespDTO point : newPoints) {
123   - Long pointId = point.getId();
124   - Integer newPollInterval = point.getPollInterval();
125   - PointTimerInfo existingTimer = currentTimers.get(pointId);
126   - // 3.1 新增点位:创建定时器
127   - if (existingTimer == null) {
128   - Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
129   - if (timerId != null) {
130   - currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
131   - log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
132   - deviceId, pointId, newPollInterval);
133   - }
134   - } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
135   - // 3.2 pollInterval 变化:重建定时器
136   - vertx.cancelTimer(existingTimer.getTimerId());
137   - Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
138   - if (timerId != null) {
139   - currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
140   - log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
141   - deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
142   - } else {
143   - currentTimers.remove(pointId);
144   - }
145   - }
146   - // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置
147   - }
148   - }
149   -
150   - /**
151   - * 创建轮询定时器
152   - * <p>
153   - * 闭包只捕获 deviceId 和 pointId,运行时从 configCache 获取最新配置,避免旧快照问题。
154   - */
155   - private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) {
156   - if (pollInterval == null || pollInterval <= 0) {
157   - return null;
158   - }
159   - return vertx.setPeriodic(pollInterval, timerId -> {
160   - try {
161   - submitPollRequest(deviceId, pointId);
162   - } catch (Exception e) {
163   - log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e);
164   - }
165   - });
166   - }
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 36 // ========== 轮询执行 ==========
235 37  
236 38 /**
237 39 * 轮询单个点位
238 40 */
239   - private void pollPoint(Long deviceId, Long pointId) {
  41 + @Override
  42 + protected void pollPoint(Long deviceId, Long pointId) {
240 43 // 1.1 从 configCache 获取最新配置
241 44 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
242 45 if (config == null || CollUtil.isEmpty(config.getPoints())) {
... ... @@ -244,8 +47,7 @@ public class IotModbusTcpPollScheduler {
244 47 return;
245 48 }
246 49 // 1.2 查找点位
247   - // TODO @AI:是不是这里,可以抽到 IotModbusUtils 里?感觉应该有几个地方需要的;
248   - IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId));
  50 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId);
249 51 if (point == null) {
250 52 log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId);
251 53 return;
... ... @@ -259,45 +61,13 @@ public class IotModbusTcpPollScheduler {
259 61 }
260 62 // 2.2 获取 slave ID
261 63 Integer slaveId = connectionManager.getSlaveId(deviceId);
262   - if (slaveId == null) {
263   - log.warn("[pollPoint][设备 {} 没有 slaveId]", deviceId);
264   - return;
265   - }
  64 + Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId);
266 65  
267 66 // 3. 执行 Modbus 读取
268   - IotModbusTcpClientUtils.read(connection, slaveId, point)
  67 + IotModbusTcpMasterUtils.read(connection, slaveId, point)
269 68 .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue))
270 69 .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]",
271 70 deviceId, point.getIdentifier(), e));
272 71 }
273 72  
274   - // ========== 停止 ==========
275   -
276   - /**
277   - * 停止设备的轮询
278   - */
279   - public void stopPolling(Long deviceId) {
280   - Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
281   - if (CollUtil.isEmpty(timers)) {
282   - return;
283   - }
284   - for (PointTimerInfo timerInfo : timers.values()) {
285   - vertx.cancelTimer(timerInfo.getTimerId());
286   - }
287   - // 清理请求队列
288   - deviceRequestQueues.remove(deviceId);
289   - deviceLastRequestTime.remove(deviceId);
290   - deviceDelayTimerActive.remove(deviceId);
291   - log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
292   - }
293   -
294   - /**
295   - * 停止所有轮询
296   - */
297   - public void stopAll() {
298   - for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
299   - stopPolling(deviceId);
300   - }
301   - }
302   -
303 73 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
4   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
5 5 import lombok.Data;
6 6 import lombok.experimental.Accessors;
7 7  
... ... @@ -39,7 +39,7 @@ public class IotModbusFrame {
39 39 * 当功能码最高位为 1 时(异常响应),此字段存储异常码。
40 40 * 为 null 表示非异常响应。
41 41 *
42   - * @see IotModbusUtils#FC_EXCEPTION_MASK
  42 + * @see IotModbusCommonUtils#FC_EXCEPTION_MASK
43 43 */
44 44 private Integer exceptionCode;
45 45  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
4   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
5 5 import io.vertx.core.Handler;
6 6 import io.vertx.core.buffer.Buffer;
7 7 import io.vertx.core.parsetools.RecordParser;
... ... @@ -96,7 +96,7 @@ public class IotModbusFrameDecoder {
96 96 return null;
97 97 }
98 98 // 校验 CRC
99   - if (!IotModbusUtils.verifyCrc16(data)) {
  99 + if (!IotModbusCommonUtils.verifyCrc16(data)) {
100 100 log.warn("[decodeRtuResponse][CRC 校验失败]");
101 101 return null;
102 102 }
... ... @@ -119,8 +119,8 @@ public class IotModbusFrameDecoder {
119 119 .setPdu(pdu)
120 120 .setTransactionId(transactionId);
121 121 // 异常响应
122   - if (IotModbusUtils.isExceptionResponse(functionCode)) {
123   - frame.setFunctionCode(IotModbusUtils.extractOriginalFunctionCode(functionCode));
  122 + if (IotModbusCommonUtils.isExceptionResponse(functionCode)) {
  123 + frame.setFunctionCode(IotModbusCommonUtils.extractOriginalFunctionCode(functionCode));
124 124 if (pdu.length >= 1) {
125 125 frame.setExceptionCode(pdu[0] & 0xFF);
126 126 }
... ... @@ -281,7 +281,7 @@ public class IotModbusFrameDecoder {
281 281 this.slaveId = bytes[0];
282 282 this.functionCode = bytes[1];
283 283 int fc = functionCode & 0xFF;
284   - if (IotModbusUtils.isExceptionResponse(fc)) {
  284 + if (IotModbusCommonUtils.isExceptionResponse(fc)) {
285 285 // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节
286 286 // 已有 6 字节(多 1 字节),取前 5 字节组装
287 287 Buffer frame = Buffer.buffer(5);
... ... @@ -290,7 +290,7 @@ public class IotModbusFrameDecoder {
290 290 frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC
291 291 emitFrame(frame);
292 292 resetToHeader();
293   - } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) {
  293 + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) {
294 294 // 读响应或自定义 FC:bytes[2] = byteCount
295 295 this.byteCount = bytes[2];
296 296 int bc = byteCount & 0xFF;
... ... @@ -315,7 +315,7 @@ public class IotModbusFrameDecoder {
315 315 this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC
316 316 parser.fixedSizeMode(remaining);
317 317 }
318   - } else if (IotModbusUtils.isWriteResponse(fc)) {
  318 + } else if (IotModbusCommonUtils.isWriteResponse(fc)) {
319 319 // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节
320 320 // 已有 6 字节,还需 2 字节
321 321 state = STATE_WRITE_BODY;
... ... @@ -356,15 +356,15 @@ public class IotModbusFrameDecoder {
356 356 this.slaveId = header[0];
357 357 this.functionCode = header[1];
358 358 int fc = functionCode & 0xFF;
359   - if (IotModbusUtils.isExceptionResponse(fc)) {
  359 + if (IotModbusCommonUtils.isExceptionResponse(fc)) {
360 360 // 异常响应
361 361 state = STATE_EXCEPTION_BODY;
362 362 parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2)
363   - } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) {
  363 + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) {
364 364 // 读响应或自定义 FC
365 365 state = STATE_READ_BYTE_COUNT;
366 366 parser.fixedSizeMode(1); // byteCount
367   - } else if (IotModbusUtils.isWriteResponse(fc)) {
  367 + } else if (IotModbusCommonUtils.isWriteResponse(fc)) {
368 368 // 写响应
369 369 state = STATE_WRITE_BODY;
370 370 pendingData = Buffer.buffer();
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
4   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
5 5 import lombok.RequiredArgsConstructor;
6 6 import lombok.extern.slf4j.Slf4j;
7 7  
... ... @@ -60,7 +60,7 @@ public class IotModbusFrameEncoder {
60 60 public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value,
61 61 IotModbusFrameFormatEnum format, Integer transactionId) {
62 62 // FC05 单写线圈:Modbus 标准要求 value 为 0xFF00(ON)或 0x0000(OFF)
63   - if (functionCode == IotModbusUtils.FC_WRITE_SINGLE_COIL) {
  63 + if (functionCode == IotModbusCommonUtils.FC_WRITE_SINGLE_COIL) {
64 64 value = (value != 0) ? 0xFF00 : 0x0000;
65 65 }
66 66 // PDU: [FC(1)] [Address(2)] [Value(2)]
... ... @@ -120,7 +120,7 @@ public class IotModbusFrameEncoder {
120 120 int quantity = values.length;
121 121 int byteCount = (quantity + 7) / 8; // 向上取整
122 122 byte[] pdu = new byte[6 + byteCount];
123   - pdu[0] = (byte) IotModbusUtils.FC_WRITE_MULTIPLE_COILS; // FC15
  123 + pdu[0] = (byte) IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS; // FC15
124 124 pdu[1] = (byte) ((address >> 8) & 0xFF);
125 125 pdu[2] = (byte) (address & 0xFF);
126 126 pdu[3] = (byte) ((quantity >> 8) & 0xFF);
... ... @@ -204,7 +204,7 @@ public class IotModbusFrameEncoder {
204 204 frame[0] = (byte) slaveId;
205 205 System.arraycopy(pdu, 0, frame, 1, pdu.length);
206 206 // 计算并追加 CRC16
207   - int crc = IotModbusUtils.calculateCrc16(frame, frame.length - 2);
  207 + int crc = IotModbusCommonUtils.calculateCrc16(frame, frame.length - 2);
208 208 frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low
209 209 frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
210 210 return frame;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
... ... @@ -7,7 +7,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
7 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 8 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
9 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
11 11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
12 12 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
13 13 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
... ... @@ -82,13 +82,13 @@ public class IotModbusTcpSlaveDownstreamHandler {
82 82 String identifier = entry.getKey();
83 83 Object value = entry.getValue();
84 84 // 2.1 查找对应的点位配置
85   - IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier);
  85 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier);
86 86 if (point == null) {
87 87 log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
88 88 continue;
89 89 }
90 90 // 2.2 检查是否支持写操作
91   - if (!IotModbusUtils.isWritable(point.getFunctionCode())) {
  91 + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) {
92 92 log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
93 93 continue;
94 94 }
... ... @@ -104,7 +104,7 @@ public class IotModbusTcpSlaveDownstreamHandler {
104 104 private void writeProperty(Long deviceId, ConnectionInfo connInfo,
105 105 IotModbusPointRespDTO point, Object value) {
106 106 // 1.1 转换属性值为原始值
107   - int[] rawValues = IotModbusUtils.convertToRawValues(value, point);
  107 + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point);
108 108  
109 109 // 1.2 确定帧格式和事务 ID
110 110 IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
... ... @@ -117,8 +117,8 @@ public class IotModbusTcpSlaveDownstreamHandler {
117 117 // 1.3 编码写请求
118 118 byte[] data;
119 119 int readFunctionCode = point.getFunctionCode();
120   - Integer writeSingleCode = IotModbusUtils.getWriteSingleFunctionCode(readFunctionCode);
121   - Integer writeMultipleCode = IotModbusUtils.getWriteMultipleFunctionCode(readFunctionCode);
  120 + Integer writeSingleCode = IotModbusCommonUtils.getWriteSingleFunctionCode(readFunctionCode);
  121 + Integer writeMultipleCode = IotModbusCommonUtils.getWriteMultipleFunctionCode(readFunctionCode);
122 122 if (rawValues.length == 1 && writeSingleCode != null) {
123 123 // 单个值:使用单写功能码(FC05/FC06)
124 124 data = frameEncoder.encodeWriteSingleRequest(slaveId, writeSingleCode,
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.dow
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotModbusTcpSlaveDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotModbusTcpSlaveDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotModbusTcpSlaveDownstreamHandler downstreamHandler;
18 18  
... ...
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,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
19 19 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
20 20 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
21 21 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
22   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
  22 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
23 23 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
24 24 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
25 25 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
... ... @@ -237,7 +237,7 @@ public class IotModbusTcpSlaveUpstreamHandler {
237 237 return;
238 238 }
239 239 // 2.2 提取寄存器值
240   - int[] rawValues = IotModbusUtils.extractValues(frame);
  240 + int[] rawValues = IotModbusCommonUtils.extractValues(frame);
241 241 if (rawValues == null) {
242 242 log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]",
243 243 info.getDeviceId(), request.getIdentifier());
... ... @@ -248,14 +248,13 @@ public class IotModbusTcpSlaveUpstreamHandler {
248 248 if (config == null || CollUtil.isEmpty(config.getPoints())) {
249 249 return;
250 250 }
251   - IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(),
252   - p -> p.getId().equals(request.getPointId()));
  251 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, request.getPointId());
253 252 if (point == null) {
254 253 return;
255 254 }
256 255  
257 256 // 3.1 点位翻译
258   - Object convertedValue = IotModbusUtils.convertToPropertyValue(rawValues, point);
  257 + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValues, point);
259 258 // 3.2 上报属性
260 259 Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue);
261 260 IotDeviceMessage message = IotDeviceMessage.requestOf(
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConnectionManager.java
... ... @@ -75,8 +75,8 @@ public class IotModbusTcpSlaveConnectionManager {
75 75 try {
76 76 oldSocket.close();
77 77 } catch (Exception e) {
78   - // TODO @AI:这里日志可以打的更完整一点,方便追溯;比如:设备 ID、旧连接地址等
79   - log.warn("[registerConnection][关闭旧 socket 失败]", e);
  78 + log.warn("[registerConnection][关闭旧 socket 失败, deviceId={}, oldRemote={}]",
  79 + info.getDeviceId(), oldSocket.remoteAddress(), e);
80 80 }
81 81 }
82 82  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlavePollScheduler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
4 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6 7 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
7 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
8 11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
9 12 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlavePendingRequestManager.PendingRequest;
10 13 import io.vertx.core.Vertx;
11   -import lombok.AllArgsConstructor;
12   -import lombok.Data;
13 14 import lombok.extern.slf4j.Slf4j;
14 15  
15   -import java.util.*;
16   -import java.util.concurrent.ConcurrentHashMap;
17   -import java.util.concurrent.ConcurrentLinkedQueue;
18 16 import java.util.concurrent.atomic.AtomicInteger;
19 17  
20   -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
21   -
22 18 /**
23   - * IoT Modbus TCP Slave 轮询调度器
24   - * <p>
25   - * 管理点位的轮询定时器,为云端轮询模式的设备调度读取任务。
26   - * 与 tcpmaster 的 {@code IotModbusTcpPollScheduler} 不同,这里不通过 j2mod 直接读取,而是:
27   - * 1. 编码 Modbus 读请求帧
28   - * 2. 通过 ConnectionManager 发送到设备的 TCP 连接
29   - * 3. 将请求注册到 PendingRequestManager,等待设备响应
30   - * <p>
31   - * 闭包只捕获 deviceId + pointId,运行时从 configCacheService 获取最新 config 和 point,
32   - * 避免闭包捕获旧快照导致上报消息用旧身份的问题。
  19 + * IoT Modbus TCP Slave 轮询调度器:编码读请求帧,通过 TCP 连接发送到设备,注册 PendingRequest 等待响应
33 20 *
34 21 * @author 芋道源码
35 22 */
36 23 @Slf4j
37   -public class IotModbusTcpSlavePollScheduler {
  24 +public class IotModbusTcpSlavePollScheduler extends AbstractIotModbusPollScheduler {
38 25  
39   - private final Vertx vertx;
40 26 private final IotModbusTcpSlaveConnectionManager connectionManager;
41 27 private final IotModbusFrameEncoder frameEncoder;
42 28 private final IotModbusTcpSlavePendingRequestManager pendingRequestManager;
... ... @@ -47,29 +33,6 @@ public class IotModbusTcpSlavePollScheduler {
47 33 */
48 34 private final AtomicInteger transactionIdCounter;
49 35  
50   - /**
51   - * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积
52   - */
53   - private static final long MIN_REQUEST_INTERVAL = 200;
54   -
55   - /**
56   - * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
57   - */
58   - private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
59   -
60   - /**
61   - * per-device 请求队列:deviceId -> 待执行请求队列
62   - */
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 36 public IotModbusTcpSlavePollScheduler(Vertx vertx,
74 37 IotModbusTcpSlaveConnectionManager connectionManager,
75 38 IotModbusFrameEncoder frameEncoder,
... ... @@ -77,7 +40,7 @@ public class IotModbusTcpSlavePollScheduler {
77 40 int requestTimeout,
78 41 AtomicInteger transactionIdCounter,
79 42 IotModbusTcpSlaveConfigCacheService configCacheService) {
80   - this.vertx = vertx;
  43 + super(vertx);
81 44 this.connectionManager = connectionManager;
82 45 this.frameEncoder = frameEncoder;
83 46 this.pendingRequestManager = pendingRequestManager;
... ... @@ -86,185 +49,13 @@ public class IotModbusTcpSlavePollScheduler {
86 49 this.configCacheService = configCacheService;
87 50 }
88 51  
89   - /**
90   - * 点位定时器信息
91   - */
92   - @Data
93   - @AllArgsConstructor
94   - private static class PointTimerInfo {
95   -
96   - /**
97   - * Vert.x 定时器 ID
98   - */
99   - private Long timerId;
100   - /**
101   - * 轮询间隔(用于判断是否需要更新定时器)
102   - */
103   - private Integer pollInterval;
104   -
105   - }
106   -
107   - // ========== 轮询管理 ==========
108   -
109   - /**
110   - * 更新轮询任务(增量更新)
111   - *
112   - * 1. 【删除】点位:停止对应的轮询定时器
113   - * 2. 【新增】点位:创建对应的轮询定时器
114   - * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器
115   - * 4. 其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point)
116   - */
117   - public void updatePolling(IotModbusDeviceConfigRespDTO config) {
118   - Long deviceId = config.getDeviceId();
119   - List<IotModbusPointRespDTO> newPoints = config.getPoints();
120   - Map<Long, PointTimerInfo> currentTimers = devicePointTimers
121   - .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
122   - // 1.1 计算新配置中的点位 ID 集合
123   - Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
124   - // 1.2 计算删除的点位 ID 集合
125   - Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
126   - removedPointIds.removeAll(newPointIds);
127   -
128   - // 2. 处理删除的点位:停止不再存在的定时器
129   - for (Long pointId : removedPointIds) {
130   - PointTimerInfo timerInfo = currentTimers.remove(pointId);
131   - if (timerInfo != null) {
132   - vertx.cancelTimer(timerInfo.getTimerId());
133   - log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
134   - }
135   - }
136   -
137   - // 3. 处理新增和修改的点位
138   - if (CollUtil.isEmpty(newPoints)) {
139   - return;
140   - }
141   - for (IotModbusPointRespDTO point : newPoints) {
142   - Long pointId = point.getId();
143   - Integer newPollInterval = point.getPollInterval();
144   - PointTimerInfo existingTimer = currentTimers.get(pointId);
145   - // 3.1 新增点位:创建定时器
146   - if (existingTimer == null) {
147   - Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
148   - if (timerId != null) {
149   - currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
150   - log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
151   - deviceId, pointId, newPollInterval);
152   - }
153   - } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
154   - // 3.2 pollInterval 变化:重建定时器
155   - vertx.cancelTimer(existingTimer.getTimerId());
156   - Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
157   - if (timerId != null) {
158   - currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
159   - log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
160   - deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
161   - } else {
162   - currentTimers.remove(pointId);
163   - }
164   - }
165   - // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置
166   - }
167   - }
168   -
169   - /**
170   - * 创建轮询定时器
171   - * <p>
172   - * 闭包只捕获 deviceId 和 pointId,运行时从 configCache 获取最新配置,避免旧快照问题。
173   - */
174   - private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) {
175   - if (pollInterval == null || pollInterval <= 0) {
176   - return null;
177   - }
178   - return vertx.setPeriodic(pollInterval, timerId -> {
179   - try {
180   - submitPollRequest(deviceId, pointId);
181   - } catch (Exception e) {
182   - log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e);
183   - }
184   - });
185   - }
186   -
187   - // ========== 请求队列(per-device 限速) ==========
188   -
189   - /**
190   - * 提交轮询请求到设备请求队列(保证同设备请求间隔)
191   - */
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 52 // ========== 轮询执行 ==========
261 53  
262 54 /**
263 55 * 轮询单个点位
264   - * <p>
265   - * 运行时从 configCacheService 获取最新的 config 和 point,而非使用闭包捕获的旧引用。
266 56 */
267   - private void pollPoint(Long deviceId, Long pointId) {
  57 + @Override
  58 + protected void pollPoint(Long deviceId, Long pointId) {
268 59 // 1.1 从 configCache 获取最新配置
269 60 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
270 61 if (config == null || CollUtil.isEmpty(config.getPoints())) {
... ... @@ -272,34 +63,31 @@ public class IotModbusTcpSlavePollScheduler {
272 63 return;
273 64 }
274 65 // 1.2 查找点位
275   - IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId));
  66 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId);
276 67 if (point == null) {
277 68 log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId);
278 69 return;
279 70 }
280 71  
281   - // 2. 获取连接信息
282   - ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(deviceId);
283   - if (connInfo == null) {
  72 + // 2.1 获取连接
  73 + ConnectionInfo connection = connectionManager.getConnectionInfoByDeviceId(deviceId);
  74 + if (connection == null) {
284 75 log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId);
285 76 return;
286 77 }
  78 + // 2.2 获取 slave ID
  79 + IotModbusFrameFormatEnum frameFormat = connection.getFrameFormat();
  80 + Assert.notNull(frameFormat, "设备 {} 的帧格式不能为空", deviceId);
  81 + int slaveId = connection.getSlaveId();
  82 + Assert.notNull(connection.getSlaveId(), "设备 {} 的 slaveId 不能为空", deviceId);
287 83  
288   - // 3.1 确定帧格式和事务 ID
289   - IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
290   - if (frameFormat == null) {
291   - log.warn("[pollPoint][设备 {} 帧格式为空,跳过轮询]", deviceId);
292   - return;
293   - }
  84 + // 3.1 编码读请求
294 85 Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP
295 86 ? (transactionIdCounter.incrementAndGet() & 0xFFFF)
296 87 : null;
297   - // TODO @AI:这里断言必须非空!
298   - int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
299   - // 3.2 编码读请求
300 88 byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(),
301 89 point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId);
302   - // 3.3 注册 PendingRequest
  90 + // 3.2 注册 PendingRequest
303 91 PendingRequest pendingRequest = new PendingRequest(
304 92 deviceId, point.getId(), point.getIdentifier(),
305 93 slaveId, point.getFunctionCode(),
... ... @@ -307,41 +95,11 @@ public class IotModbusTcpSlavePollScheduler {
307 95 transactionId,
308 96 System.currentTimeMillis() + requestTimeout);
309 97 pendingRequestManager.addRequest(pendingRequest);
310   -
311   - // 4. 发送读请求
  98 + // 3.3 发送读请求
312 99 connectionManager.sendToDevice(deviceId, data);
313 100 log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]",
314 101 deviceId, point.getIdentifier(), point.getFunctionCode(),
315 102 point.getRegisterAddress(), point.getRegisterCount());
316 103 }
317 104  
318   - // ========== 停止 ==========
319   -
320   - /**
321   - * 停止设备的轮询
322   - */
323   - public void stopPolling(Long deviceId) {
324   - Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
325   - if (CollUtil.isEmpty(timers)) {
326   - return;
327   - }
328   - for (PointTimerInfo timerInfo : timers.values()) {
329   - vertx.cancelTimer(timerInfo.getTimerId());
330   - }
331   - // 清理请求队列
332   - deviceRequestQueues.remove(deviceId);
333   - deviceLastRequestTime.remove(deviceId);
334   - deviceDelayTimerActive.remove(deviceId);
335   - log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
336   - }
337   -
338   - /**
339   - * 停止所有轮询
340   - */
341   - public void stopAll() {
342   - for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
343   - stopPolling(deviceId);
344   - }
345   - }
346   -
347 105 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotMqttDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotMqttDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream;
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotTcpDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream;
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotUdpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotUdpDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstrea
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotWebSocketDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotWebSocketDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotWebSocketDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java
... ... @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
8 8 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder;
10 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
11   -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
12 12 import io.vertx.core.Vertx;
13 13 import io.vertx.core.buffer.Buffer;
14 14 import io.vertx.core.net.NetClient;
... ... @@ -285,7 +285,7 @@ public class IotModbusTcpSlaveModbusRtuIntegrationTest {
285 285 frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF);
286 286 }
287 287 // 计算 CRC16
288   - int crc = IotModbusUtils.calculateCrc16(frame, totalLength - 2);
  288 + int crc = IotModbusCommonUtils.calculateCrc16(frame, totalLength - 2);
289 289 frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low
290 290 frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
291 291 return frame;
... ...